diff --git a/.github/workflows/bats_tests.yml b/.github/workflows/bats_tests.yml new file mode 100644 index 0000000..ad92e10 --- /dev/null +++ b/.github/workflows/bats_tests.yml @@ -0,0 +1,104 @@ +name: Bats Tests + +on: + pull_request: + branches: + - main + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }}-bats-tests + cancel-in-progress: true + +jobs: + actionlint: + name: Validate GitHub Workflows (actionlint) + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install actionlint + run: | + ACTIONLINT_VERSION="1.7.7" + curl -sSfL "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" \ + | tar -xz + sudo mv actionlint /usr/local/bin/actionlint + actionlint -version + + - name: Run Workflow Linter + run: actionlint + + shellcheck: + name: Lint Shell Scripts (shellcheck) + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install shellcheck + run: | + sudo apt-get update -y + sudo apt-get install -y shellcheck + + - name: Run Shell Linter + run: shellcheck scripts/*.sh + + shfmt: + name: Check Shell Formatting (shfmt) + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install shfmt + run: | + sudo apt-get update -y + sudo apt-get install -y shfmt + + - name: Run Shell Formatter Check + run: git ls-files '*.sh' '*.bash' '*.bats' | xargs shfmt -d -i 4 -ci + + bats: + name: Run Shell Test Suite (bats) + needs: + - shellcheck + - shfmt + - actionlint + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macos-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install bats (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update -y + sudo apt-get install -y bats + + - name: Install bats (macOS) + if: runner.os == 'macOS' + run: brew install bats-core + + - name: Run bats Test Suite + run: bats --recursive tests/bats diff --git a/.github/workflows/extra_soundness.yml b/.github/workflows/extra_soundness.yml index b6d4d91..9317738 100644 --- a/.github/workflows/extra_soundness.yml +++ b/.github/workflows/extra_soundness.yml @@ -15,6 +15,10 @@ on: type: boolean description: "Boolean to enable run tests with .build cache." default: false + tests_cache_enabled: + type: boolean + description: "Boolean to enable .build cache usage inside the run tests job." + default: true run_tests_swift_versions: type: string description: "List of Swift versions to test with." @@ -126,11 +130,13 @@ jobs: ssh-keyscan github.com >> ~/.ssh/known_hosts - name: Install zstd + if: ${{ inputs.tests_cache_enabled }} run: | sudo apt-get update -y sudo apt-get install -y zstd - name: Restore .build + if: ${{ inputs.tests_cache_enabled }} id: "restore-build" uses: actions/cache/restore@v4 with: @@ -142,11 +148,16 @@ jobs: run: swift build --build-tests - name: Cache .build - if: steps.restore-build.outputs.cache-hit != 'true' + if: ${{ inputs.tests_cache_enabled && steps.restore-build.outputs.cache-hit != 'true' }} uses: actions/cache/save@v4 with: path: .build key: "swiftpm-tests-build-${{ runner.os }}-${{ matrix.swift }}-${{ github.event.pull_request.base.sha || github.event.after }}" - name: Run unit tests - run: swift test --skip-build --parallel + run: | + if [ "${{ inputs.tests_cache_enabled }}" = "true" ]; then + swift test --skip-build --parallel + else + swift test --parallel + fi diff --git a/Makefile b/Makefile index 47b79c9..beb0b09 100644 --- a/Makefile +++ b/Makefile @@ -66,4 +66,16 @@ format: package: curl -s $(baseUrl)/check-swift-package.sh | bash +lint-shell: + curl -s $(baseUrl)/script-format.sh | bash + +format-shell: + curl -s $(baseUrl)/script-format.sh | bash -s -- --fix + +lint-workflows: + curl -s $(baseUrl)/run-actionlint.sh | bash + check: symlinks language deps lint docc-warnings headers + +test-bats: + bats --recursive tests/bats diff --git a/README.md b/README.md index d38ec73..cc1902c 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ This workflow provides configurable, robust checks and testing: * **Optional Local Swift Dependency Checks**: Checks for accidental `.package(path:)` usage. * **Optional Swift Headers Check**: Validates Swift source file headers using a strict 5-line format and respects `.swiftheaderignore`. * **Optional DocC Warnings Check**: Runs DocC analysis with `--warnings-as-errors` and fails on warnings. -* **Optional Swift Test Execution**: Runs tests using **`.build` caching** for efficiency. +* **Optional Swift Test Execution**: Runs tests with optional **`.build` caching** for efficiency. * **Optional Swift Package Validation**: Validates Swift package structure, settings, and conventions to ensure consistency. * **Multi-Version Support**: Tests across multiple Swift versions, configurable via input (defaulting to `["6.0", "6.1"]`). * **SSH Support**: Includes steps to set up **SSH credentials** (via the `SSH_PRIVATE_KEY` secret) for projects relying on private Git dependencies. @@ -38,7 +38,8 @@ This workflow provides configurable, robust checks and testing: | `local_swift_dependencies_check_enabled` | Enables local Swift dependency check | `false` | | `headers_check_enabled` | Enables Swift headers validation | `false` | | `docc_warnings_check_enabled` | Enables DocC warnings check | `false` | -| `run_tests_with_cache_enabled` | Enables Swift tests with `.build` cache | `false` | +| `run_tests_with_cache_enabled` | Enables the Swift test job | `false` | +| `tests_cache_enabled` | Enables `.build` cache restore/save in the Swift test job | `true` | | `run_tests_swift_versions` | Swift versions to test | `["6.1","6.2"]` | | `swift_package_validation_enabled` | Runs Swift package validation in the repository. | `false` | @@ -53,6 +54,7 @@ jobs: headers_check_enabled: true docc_warnings_check_enabled: true run_tests_with_cache_enabled: true + tests_cache_enabled: true run_tests_swift_versions: '["6.1","6.2"]' swift_package_validation_enabled: true ``` @@ -146,8 +148,8 @@ Detects source- and binary-level API breaking changes in Swift packages to preve #### Behavior * Uses `swift package diagnose-api-breaking-changes` -* Pull requests: compares against the PR base branch -* Other contexts: compares against the latest Git tag +* Pull requests: fetches `${GITHUB_BASE_REF}` into a local `pull-base-ref` and compares against that ref +* Other contexts: fetches tags and compares against the latest Git tag * If no tags exist, exits successfully with a warning * Fails when breaking changes are detected @@ -233,7 +235,7 @@ Prevents accidental usage of local Swift package dependencies. #### Behavior -* Scans all tracked `Package.swift` files +* Scans git-tracked `Package.swift` files only (ignores untracked files) * Detects `.package(path:)` * Fails immediately on detection @@ -257,27 +259,36 @@ curl -s $(baseUrl)/check-local-swift-dependencies.sh | bash #### Purpose -Runs a security scan of an OpenAPI specification using OWASP ZAP. +Runs fast static OpenAPI security lint checks using Spectral. #### Behavior * Executes inside Docker -* Skips execution if `openapi/` directory does not exist +* Accepts an OpenAPI file or directory (default: `openapi`) +* Relative `-f` paths are resolved from the git repository root first, then current working directory +* For file paths, supports `.yml`/`.yaml`/`.json` extension fallback +* For directory paths, resolves `openapi.yaml`, then `openapi.yml`, then `openapi.json` +* Performs static linting only (no running API server required) +* Skips execution if no OpenAPI specification can be resolved #### Parameters -_None_ +* `-f ` – OpenAPI file or directory path #### Ignore files _None_ -#### Raw curl example +#### Raw curl examples ```sh curl -s $(baseUrl)/check-openapi-security.sh | bash ``` +```sh +curl -s $(baseUrl)/check-openapi-security.sh | bash -s -- -f openapi/openapi.yml +``` + --- ### check-openapi-validation.sh @@ -289,22 +300,39 @@ Validates an OpenAPI specification for schema correctness. #### Behavior * Runs the OpenAPI validator in Docker -* Skips execution if `openapi/` directory does not exist +* Accepts an OpenAPI file or directory (default: `openapi`) +* Relative `-f` paths are resolved from the git repository root first, then current working directory +* For file paths, supports `.yml`/`.yaml`/`.json` extension fallback +* For directory paths, resolves `openapi.yaml`, then `openapi.yml`, then `openapi.json` +* Supports debug tracing via `-d` (prints resolved paths and command trace) +* Supports `--detailed` to run additional Spectral diagnostics when validation fails +* Skips execution if no OpenAPI specification can be resolved #### Parameters -_None_ +* `-f ` – OpenAPI file or directory path +* `-d` – enable debug tracing +* `--detailed` – run additional detailed diagnostics on validation failure #### Ignore files _None_ -#### Raw curl example +#### Raw curl examples ```sh curl -s $(baseUrl)/check-openapi-validation.sh | bash ``` +```sh +curl -s $(baseUrl)/check-openapi-validation.sh | bash -s -- -f openapi/openapi.yaml +``` + +```sh +# Monorepo/nested project example (path relative to git root) +curl -s $(baseUrl)/check-openapi-validation.sh | bash -s -- -f mail-examples/mail-example-openapi/openapi/openapi.yaml +``` + --- ### check-swift-headers.sh @@ -318,6 +346,10 @@ Ensures Swift source files contain a consistent, standardized header. * Enforces a strict 5-line header format * Can optionally insert or update headers in-place * Processes only git-tracked Swift files +* Skips `Package.swift` explicitly +* Accepts `Created by ... on YYYY. MM. DD.` and legacy `..` suffix +* In `--fix` mode, normalizes legacy `..` to `.` +* When repairing malformed headers, preserves extracted author and date when possible #### Parameters @@ -326,7 +358,7 @@ Ensures Swift source files contain a consistent, standardized header. #### Ignore files -* `.swiftheaderignore` – excludes paths from header validation +* `.swiftheaderignore` – excludes paths from header validation (replaces default exclusions when present) #### Raw curl examples @@ -354,6 +386,7 @@ Detects discouraged or outdated terminology to promote inclusive language. * Case-insensitive, whole-word matching * Scans git-tracked files only +* Lines containing `ignore-unacceptable-language` are excluded from failures #### Parameters @@ -379,9 +412,11 @@ Generates a CONTRIBUTORS.txt file from git commit history. #### Behavior -* Uses `git shortlog` +* Uses `git shortlog -es HEAD` * Respects `.mailmap` * Overwrites the file deterministically +* Writes to repository root even when run from a subdirectory +* If the repository has no commits, exits successfully and does not create `CONTRIBUTORS.txt` #### Parameters @@ -472,11 +507,15 @@ Installs the Swift OpenAPI Generator CLI tool. #### Behavior * Builds from source -* Supports version pinning +* Installs the latest available tag by default +* Supports version pinning via `-v` +* Validates required tools (`git`, `curl`, `tar`, `swift`, `install`) +* Uses fail-fast download behavior for release archives #### Parameters * `-v ` – install a specific version +* `-h` – show usage help #### Ignore files @@ -506,7 +545,8 @@ Removes generated build artifacts and temporary files. ⚠️ Irreversible opera #### Behavior -* Deletes `.build`, `.swiftpm`, and generated files +* Deletes `.build/` and `.swiftpm/` +* Deletes `openapi/openapi.yaml`, `db.sqlite`, and `migration-entries.json` * Intended for local development use @@ -563,6 +603,10 @@ Serves OpenAPI documentation locally using Docker. #### Behavior +* Accepts an OpenAPI file or directory (default: `openapi`) +* Relative `-f` paths are resolved from the git repository root first, then current working directory +* If a file is provided, mounts its parent directory +* For file paths, supports `.yml`/`.yaml`/`.json` extension fallback * Runs Nginx in the foreground * Exposes documentation over HTTP @@ -570,17 +614,22 @@ Serves OpenAPI documentation locally using Docker. * `-n ` – container name * `-p ` – port mapping +* `-f ` – OpenAPI file or directory path #### Ignore files _None_ -#### Raw curl example +#### Raw curl examples ```sh curl -s $(baseUrl)/run-openapi-docker.sh | bash -s -- -n openapi-preview ``` +```sh +curl -s $(baseUrl)/run-openapi-docker.sh | bash -s -- -n openapi-preview -f openapi/openapi.yaml +``` + --- ### run-swift-format.sh @@ -619,6 +668,71 @@ curl -s $(baseUrl)/run-swift-format.sh | bash -s -- --fix --- +### run-actionlint.sh + +#### Purpose + +Runs `actionlint` to validate GitHub Actions workflows. + +#### Behavior + +* Verifies `actionlint` is installed before running +* Runs from repository root for consistent workflow path resolution +* Passes through optional CLI arguments to `actionlint` + +#### Parameters + +* `` – optional arguments passed to `actionlint` + +#### Ignore files + +_None_ + +#### Raw curl example + +```sh +curl -s $(baseUrl)/run-actionlint.sh | bash +``` + +--- + +### script-format.sh + +#### Purpose + +Runs `shfmt` to check or fix formatting for tracked shell-related files. + +#### Behavior + +* Verifies `shfmt` is installed before running +* Targets tracked `*.sh`, `*.bash`, and `*.bats` files +* Default mode checks formatting and fails on drift +* `--fix` mode applies formatting in-place + +#### Parameters + +* `--fix` – apply formatting in-place + +#### Ignore files + +_None_ + +#### Raw curl examples + +_Check only:_ + +```sh +curl -s $(baseUrl)/script-format.sh | bash +``` + +_Fix formatting:_ + +```sh +curl -s $(baseUrl)/script-format.sh | bash -s -- --fix +``` + +--- + ### check-swift-package.sh #### Purpose diff --git a/examples/tests.yml b/examples/tests.yml index 42f5085..1702a2d 100644 --- a/examples/tests.yml +++ b/examples/tests.yml @@ -43,5 +43,6 @@ jobs: local_swift_dependencies_check_enabled : true docc_warnings_check_enabled : true run_tests_with_cache_enabled : true + tests_cache_enabled : true headers_check_enabled : true run_tests_swift_versions: '["6.1","6.2"]' diff --git a/scripts/check-api-breakage.sh b/scripts/check-api-breakage.sh index 67406fa..21bfcdc 100755 --- a/scripts/check-api-breakage.sh +++ b/scripts/check-api-breakage.sh @@ -13,9 +13,12 @@ set -euo pipefail # Logging helpers -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} # Determine baseline reference if [ -n "${GITHUB_BASE_REF:-}" ] && [ -n "${GITHUB_REPOSITORY:-}" ] && [ -n "${GITHUB_SERVER_URL:-}" ]; then @@ -45,4 +48,4 @@ log "Using baseline: ${BASELINE_REF}" # This command exits non-zero if API-breaking changes are detected. swift package diagnose-api-breaking-changes "$BASELINE_REF" -log "✅ No API-breaking changes detected." \ No newline at end of file +log "✅ No API-breaking changes detected." diff --git a/scripts/check-broken-symlinks.sh b/scripts/check-broken-symlinks.sh index b5bc0a6..ee03c10 100755 --- a/scripts/check-broken-symlinks.sh +++ b/scripts/check-broken-symlinks.sh @@ -17,9 +17,12 @@ set -euo pipefail # Logging helpers # All output is written to stderr for consistent CI and local logs -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} # Resolve the repository root # Ensures correct path resolution even if the script is run from a subdirectory @@ -51,4 +54,4 @@ if [ "${NUM_BROKEN_SYMLINKS}" -gt 0 ]; then fi # Success -log "✅ Found 0 broken symlinks." \ No newline at end of file +log "✅ Found 0 broken symlinks." diff --git a/scripts/check-docc-warnings.sh b/scripts/check-docc-warnings.sh index 69cd32f..60f21d2 100755 --- a/scripts/check-docc-warnings.sh +++ b/scripts/check-docc-warnings.sh @@ -22,22 +22,25 @@ set -euo pipefail # Logging helpers # # All logs go to stderr to keep stdout clean and CI-friendly. -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} # Configuration / state -TARGETS_FILE=".docctargetlist" # Optional file defining an explicit list of DocC targets -TARGETS="" # Raw newline-separated target names (internal representation) -TARGET_LIST=() # Parsed target names as a Bash array for iteration +TARGETS_FILE=".docctargetlist" # Optional file defining an explicit list of DocC targets +TARGETS="" # Raw newline-separated target names (internal representation) +TARGET_LIST=() # Parsed target names as a Bash array for iteration # Swift package manifest to mutate PACKAGE_FILE="Package.swift" # Required injection anchor inside Package.dependencies -INJECT_MARKER='// [docc-plugin-placeholder]' +INJECT_MARKER='// [docc-plugin-placeholder]' # Dependency line injected immediately after the marker DOCC_DEP=' .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.4.0"),' - +DOCC_PLUGIN_INJECTED=false # Git safety (local runs only) # @@ -70,6 +73,27 @@ reset_git_after_analysis() { fi } +restore_injected_package_manifest() { + if [ "${DOCC_PLUGIN_INJECTED}" != "true" ]; then + return 0 + fi + + rm -f "$PACKAGE_FILE.tmp" + + if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + log "Restoring ${PACKAGE_FILE} after failed DocC analysis" + git checkout -- "$PACKAGE_FILE" || true + fi +} + +cleanup_on_exit() { + local rc=$? + if [ "$rc" -ne 0 ]; then + restore_injected_package_manifest + fi +} +trap cleanup_on_exit EXIT + # Ensures that swift-docc-plugin is available to SwiftPM. # # This function injects the dependency using a fixed marker @@ -120,13 +144,14 @@ ensure_docc_plugin() { END { if (!injected) exit 42 } - ' "$PACKAGE_FILE" > "$PACKAGE_FILE.tmp" + ' "$PACKAGE_FILE" >"$PACKAGE_FILE.tmp" mv "$PACKAGE_FILE.tmp" "$PACKAGE_FILE" + DOCC_PLUGIN_INJECTED=true # Validate manifest after mutation - swift package dump-package >/dev/null \ - || fatal "Package.swift became invalid after injecting swift-docc-plugin" + swift package dump-package >/dev/null || + fatal "Package.swift became invalid after injecting swift-docc-plugin" } # Pre-flight checks @@ -153,8 +178,8 @@ auto_detect_targets() { fatal "jq is required. Install with: brew install jq" fi - TARGETS=$(swift package dump-package \ - | jq -r '.targets[] + TARGETS=$(swift package dump-package | + jq -r '.targets[] | select(.type == "regular" or .type == "executable") | .name') @@ -177,7 +202,7 @@ fi # Convert newline-separated target list into an array while IFS= read -r TARGET; do TARGET_LIST+=("$TARGET") -done <<< "$TARGETS" +done <<<"$TARGETS" TARGET_COUNT="${#TARGET_LIST[@]}" log "Targets detected: $TARGET_COUNT" @@ -215,4 +240,4 @@ fi log "✅ All targets passed DocC analysis without warnings." -reset_git_after_analysis \ No newline at end of file +reset_git_after_analysis diff --git a/scripts/check-local-swift-dependencies.sh b/scripts/check-local-swift-dependencies.sh index 35e929e..131bc8a 100755 --- a/scripts/check-local-swift-dependencies.sh +++ b/scripts/check-local-swift-dependencies.sh @@ -21,9 +21,12 @@ set -euo pipefail # Logging helpers # # All output goes to stderr to keep stdout clean and CI-friendly. -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} # Determine repository root # @@ -39,10 +42,10 @@ REPO_ROOT="$(git -C "$PWD" rev-parse --show-toplevel)" # # Currently, we only check Package.swift, since local package references # are only valid in that file. -read -ra PATHS_TO_CHECK <<< "$( +read -ra PATHS_TO_CHECK <<<"$( git -C "${REPO_ROOT}" ls-files -z \ - "Package.swift" \ - | xargs -0 + "Package.swift" | + xargs -0 )" # Scan files for local Swift package references @@ -58,4 +61,4 @@ if [ -n "${PATHS_TO_CHECK+x}" ]; then fi # Success -log "✅ Found 0 local Swift package dependency references." \ No newline at end of file +log "✅ Found 0 local Swift package dependency references." diff --git a/scripts/check-openapi-security.sh b/scripts/check-openapi-security.sh index c0423ea..cb4bfbb 100755 --- a/scripts/check-openapi-security.sh +++ b/scripts/check-openapi-security.sh @@ -1,46 +1,156 @@ #!/usr/bin/env bash -# OpenAPI Security Check (OWASP ZAP) +# OpenAPI Security Lint (Spectral) # -# This script runs an OWASP ZAP API security scan against an OpenAPI -# specification located in the repository. +# This script runs fast static security lint checks against an OpenAPI +# specification using Spectral in Docker. # # It is designed to be: -# - Safe for CI (no side effects) +# - Fast for CI and local runs # - Optional (exits successfully if no OpenAPI spec is present) -# -# The scan helps identify common API security issues early in the pipeline. +# - Static (no running API server required) set -euo pipefail # Logging helpers -# All output goes to stderr for consistent CI logs -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} + +SCRIPT_SOURCE="${BASH_SOURCE[0]-$0}" +SCRIPT_DIR="$(cd -- "$(dirname -- "${SCRIPT_SOURCE}")" && pwd)" + +OPENAPI_PATH="openapi" +RULESET_FILE="" +RULESET_CONTAINER_PATH="/tmp/spectral-security-ruleset.yaml" + +resolve_repo_root() { + if root="$(git rev-parse --show-toplevel 2>/dev/null)"; then + printf '%s\n' "${root}" + return 0 + fi + + if [ -d "${SCRIPT_DIR}/../.git" ]; then + ( + cd -- "${SCRIPT_DIR}/.." + pwd + ) + return 0 + fi + + pwd +} + +usage() { + cat >&2 <"${RULESET_FILE}" <<'EOF' +extends: + - spectral:oas +rules: + security-requirement-defined: + description: "Define security requirements at root and/or operation level." + severity: error + given: + - "$" + then: + field: security + function: truthy + no-http-server-urls: + description: "Server URLs should use HTTPS." + severity: error + given: "$.servers[*].url" + then: + function: pattern + functionOptions: + match: "^https://" +EOF + +OPENAPI_SPEC_BASENAME="$(basename "${OPENAPI_SPEC_FILE}")" +docker run --rm --name "check-openapi-security-lint" \ + -v "${OPENAPI_SPEC_FILE}:/app/${OPENAPI_SPEC_BASENAME}" \ + -v "${RULESET_FILE}:${RULESET_CONTAINER_PATH}" \ + stoplight/spectral:latest lint "/app/${OPENAPI_SPEC_BASENAME}" \ + --fail-severity error \ + --display-only-failures \ + --ruleset "${RULESET_CONTAINER_PATH}" diff --git a/scripts/check-openapi-validation.sh b/scripts/check-openapi-validation.sh index d4ee1ba..4044ecd 100755 --- a/scripts/check-openapi-validation.sh +++ b/scripts/check-openapi-validation.sh @@ -16,32 +16,180 @@ set -euo pipefail # Logging helpers # All output is written to stderr for consistent CI logs -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} -# Resolve the repository root -# Allows the script to be run from any subdirectory -REPO_ROOT="$(git -C "$PWD" rev-parse --show-toplevel)" +# Resolve script directory +# Allows the script to be run from any subdirectory. +SCRIPT_SOURCE="${BASH_SOURCE[0]-$0}" +SCRIPT_DIR="$(cd -- "$(dirname -- "${SCRIPT_SOURCE}")" && pwd)" -# Location of the OpenAPI specification directory -# The directory is expected to contain an `openapi.yaml` file -OPENAPI_YAML_LOCATION="${REPO_ROOT}/openapi" +OPENAPI_PATH="openapi" +DEBUG=false +DETAILED=false -# If the OpenAPI directory does not exist, skip validation gracefully -# This avoids failing CI for repositories that do not define APIs -if [ ! -d "${OPENAPI_YAML_LOCATION}" ]; then - log "❗ OpenAPI location not found — skipping validation." +# Support long option for detailed diagnostics. +NORMALIZED_ARGS=() +for arg in "$@"; do + if [ "$arg" = "--detailed" ]; then + NORMALIZED_ARGS+=("-D") + else + NORMALIZED_ARGS+=("$arg") + fi +done +if [ "${#NORMALIZED_ARGS[@]}" -gt 0 ]; then + set -- "${NORMALIZED_ARGS[@]}" +else + set -- +fi + +resolve_repo_root() { + # Prefer git root for local execution and subdirectory calls. + if root="$(git rev-parse --show-toplevel 2>/dev/null)"; then + printf '%s\n' "${root}" + return 0 + fi + + # Fallback for checked-out scripts under a conventional ./scripts layout. + if [ -d "${SCRIPT_DIR}/../.git" ]; then + ( + cd -- "${SCRIPT_DIR}/.." + pwd + ) + return 0 + fi + + # Last resort for piped execution (for example: curl | bash). + pwd +} + +usage() { + cat >&2 <&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} # Configuration / state DEFAULT_AUTHOR="Binary Birds" @@ -25,66 +27,67 @@ HAS_ERRORS=0 # Argument parsing while [ $# -gt 0 ]; do - case "$1" in - --fix) - FIX_MODE=1 - ;; - --author) - shift - [ -z "${1:-}" ] && fatal "--author requires a value" - DEFAULT_AUTHOR="$1" - ;; - *) - fatal "Unknown argument: $1" - ;; - esac - shift + case "$1" in + --fix) + FIX_MODE=1 + ;; + --author) + shift + [ -z "${1:-}" ] && fatal "--author requires a value" + DEFAULT_AUTHOR="$1" + ;; + *) + fatal "Unknown argument: $1" + ;; + esac + shift done -[ "$FIX_MODE" -eq 1 ] \ - && log "Fix mode enabled — headers will be inserted or corrected." \ - || log "Checking Swift file headers..." - +if [ "$FIX_MODE" -eq 1 ]; then + log "Fix mode enabled — headers will be inserted or corrected." +else + log "Checking Swift file headers..." +fi # Project name PROJECT_NAME="$(basename "$PWD")" # Date helpers normalize_date() { - local raw="$1" - if command -v gdate >/dev/null 2>&1; then - gdate -d "$raw" +"%Y. %m. %d" 2>/dev/null - else - date -d "$raw" +"%Y. %m. %d" 2>/dev/null - fi + local raw="$1" + if command -v gdate >/dev/null 2>&1; then + gdate -d "$raw" +"%Y. %m. %d" 2>/dev/null + else + date -d "$raw" +"%Y. %m. %d" 2>/dev/null + fi } get_file_creation_date() { - local file="$1" - if git ls-files --error-unmatch "$file" >/dev/null 2>&1; then - git log -1 --format="%ad" --date=format:"%Y. %m. %d" -- "$file" - else - date +"%Y. %m. %d" - fi + local file="$1" + if git ls-files --error-unmatch "$file" >/dev/null 2>&1; then + git log -1 --format="%ad" --date=format:"%Y. %m. %d" -- "$file" + else + date +"%Y. %m. %d" + fi } # Header detection helpers # Find the line number of the "Created by" line (header anchor) find_header_line() { - grep -nE '^// Created by .+ on [0-9]{4}\. [0-9]{2}\. [0-9]{2}\.{1,2}$' "$1" \ - | head -n1 \ - | cut -d: -f1 + grep -nE '^// Created by .+ on [0-9]{4}\. [0-9]{2}\. [0-9]{2}\.{1,2}$' "$1" | + head -n1 | + cut -d: -f1 } # Validate header structure relative to the anchor line is_header_valid_at_line() { - local file="$1" - local line="$2" - local filename - filename=$(basename "$file") + local file="$1" + local line="$2" + local filename + filename=$(basename "$file") - sed -n "$((line-4)),${line}p" "$file" | awk -v f="$filename" -v p="$PROJECT_NAME" ' + sed -n "$((line - 4)),${line}p" "$file" | awk -v f="$filename" -v p="$PROJECT_NAME" ' NR==1 && $0=="//" {next} NR==2 && $0=="// " f {next} NR==3 && $0=="// " p {next} @@ -95,97 +98,110 @@ is_header_valid_at_line() { } extract_author_from_header_line() { - sed -E 's|^// Created by (.+) on [0-9]{4}\. [0-9]{2}\. [0-9]{2}\.{1,2}$|\1|' + sed -E 's|^// Created by (.+) on [0-9]{4}\. [0-9]{2}\. [0-9]{2}\.{1,2}$|\1|' } extract_date_from_header_line() { - sed -E 's|^// Created by .+ on ([0-9]{4}\. [0-9]{2}\. [0-9]{2})\.{1,2}$|\1|' + sed -E 's|^// Created by .+ on ([0-9]{4}\. [0-9]{2}\. [0-9]{2})\.{1,2}$|\1|' +} + +# Returns 0 when the header line uses legacy double-dot suffix. +is_legacy_double_dot_header_line() { + [[ "$1" =~ \.\.$ ]] } # Replace an invalid header block in place replace_header_at_line() { - local file="$1" - local line="$2" - local tmpfile - tmpfile=$(mktemp) - - # Extract original author and date - local header_line - header_line=$(sed -n "${line}p" "$file") - - local author - author=$(printf '%s\n' "$header_line" | extract_author_from_header_line) - - local date - date=$(printf '%s\n' "$header_line" | extract_date_from_header_line) - - # Safety fallback (should never happen) - [ -z "$author" ] && author="$DEFAULT_AUTHOR" - [ -z "$date" ] && date="$(get_file_creation_date "$file")" - - # Remove the old header block - local start=$((line-4)) - [ "$start" -lt 1 ] && start=1 - sed "${start},${line}d" "$file" > "$tmpfile" - - # Rebuild file WITHOUT adding extra blank lines - { - echo "//" - echo "// $(basename "$file")" - echo "// $PROJECT_NAME" - echo "//" - echo "// Created by $author on $date." - cat "$tmpfile" - } > "$tmpfile.fixed" - - mv "$tmpfile.fixed" "$file" -} + local file="$1" + local line="$2" + local tmpfile + tmpfile=$(mktemp) -# Header validation / fixing -check_or_fix_header() { - local file="$1" - local filename - filename=$(basename "$file") + # Extract original author and date + local header_line + header_line=$(sed -n "${line}p" "$file") - # Explicitly ignore Package.swift - [ "$filename" = "Package.swift" ] && return 0 + local author + author=$(printf '%s\n' "$header_line" | extract_author_from_header_line) - local header_line - header_line=$(find_header_line "$file" || true) + local date + date=$(printf '%s\n' "$header_line" | extract_date_from_header_line) - if [ -n "$header_line" ]; then - if is_header_valid_at_line "$file" "$header_line"; then - return 0 - fi + # Safety fallback (should never happen) + [ -z "$author" ] && author="$DEFAULT_AUTHOR" + [ -z "$date" ] && date="$(get_file_creation_date "$file")" - if [ "$FIX_MODE" -eq 1 ]; then - replace_header_at_line "$file" "$header_line" - log "Fixed header: $file" - else - error "❌ $file - Header is invalid" - return 1 - fi - else - # No header anywhere → insert at top - if [ "$FIX_MODE" -eq 1 ]; then - local tmpfile - tmpfile=$(mktemp) - { + # Remove the old header block + local start=$((line - 4)) + [ "$start" -lt 1 ] && start=1 + sed "${start},${line}d" "$file" >"$tmpfile" + + # Rebuild file WITHOUT adding extra blank lines + { echo "//" - echo "// $filename" + echo "// $(basename "$file")" echo "// $PROJECT_NAME" echo "//" - echo "// Created by $DEFAULT_AUTHOR on $(get_file_creation_date "$file")." - echo "" - cat "$file" - } > "$tmpfile" - mv "$tmpfile" "$file" - log "Header added: $file" + echo "// Created by $author on $date." + cat "$tmpfile" + } >"$tmpfile.fixed" + + mv "$tmpfile.fixed" "$file" +} + +# Header validation / fixing +check_or_fix_header() { + local file="$1" + local filename + filename=$(basename "$file") + + # Explicitly ignore Package.swift + [ "$filename" = "Package.swift" ] && return 0 + + local header_line + header_line=$(find_header_line "$file" || true) + + if [ -n "$header_line" ]; then + if is_header_valid_at_line "$file" "$header_line"; then + if [ "$FIX_MODE" -eq 1 ]; then + local header_text + header_text=$(sed -n "${header_line}p" "$file") + if is_legacy_double_dot_header_line "$header_text"; then + replace_header_at_line "$file" "$header_line" + log "Normalized legacy header: $file" + fi + fi + return 0 + fi + + if [ "$FIX_MODE" -eq 1 ]; then + replace_header_at_line "$file" "$header_line" + log "Fixed header: $file" + else + error "❌ $file - Header is invalid" + return 1 + fi else - error "❌ $file - Header missing" - return 1 + # No header anywhere → insert at top + if [ "$FIX_MODE" -eq 1 ]; then + local tmpfile + tmpfile=$(mktemp) + { + echo "//" + echo "// $filename" + echo "// $PROJECT_NAME" + echo "//" + echo "// Created by $DEFAULT_AUTHOR on $(get_file_creation_date "$file")." + echo "" + cat "$file" + } >"$tmpfile" + mv "$tmpfile" "$file" + log "Header added: $file" + else + error "❌ $file - Header missing" + return 1 + fi fi - fi } # Exclusions @@ -193,44 +209,52 @@ IGNORE_FILE=".swiftheaderignore" EXCLUDE_PATTERNS=() if [ -f "$IGNORE_FILE" ]; then - log "Using exclusion list from $IGNORE_FILE" - while IFS= read -r line || [ -n "$line" ]; do - [[ -n "$line" && ! "$line" =~ ^# ]] && EXCLUDE_PATTERNS+=(":(exclude)$line") - done < "$IGNORE_FILE" + log "Using exclusion list from $IGNORE_FILE" + while IFS= read -r line || [ -n "$line" ]; do + [[ -n "$line" && ! "$line" =~ ^# ]] && EXCLUDE_PATTERNS+=(":(exclude)$line") + done <"$IGNORE_FILE" else - EXCLUDE_PATTERNS+=( - ":(exclude).*" - ":(exclude)*.txt" - ":(exclude)*.png" - ":(exclude)*.jpeg" - ":(exclude)*.jpg" - ":(exclude)*.sh" - ":(exclude)*.html" - ":(exclude)*.yaml" - ":(exclude)README.md" - ":(exclude)Package.resolved" - ":(exclude)Makefile" - ":(exclude)LICENSE" - ":(exclude)Package*.swift" - ":(exclude)docker/**" - ) + EXCLUDE_PATTERNS+=( + ":(exclude).*" + ":(exclude)*.txt" + ":(exclude)*.png" + ":(exclude)*.jpeg" + ":(exclude)*.jpg" + ":(exclude)*.sh" + ":(exclude)*.html" + ":(exclude)*.yaml" + ":(exclude)README.md" + ":(exclude)Package.resolved" + ":(exclude)Makefile" + ":(exclude)LICENSE" + ":(exclude)Package*.swift" + ":(exclude)docker/**" + ) fi # File processing FILES=() while IFS= read -r -d '' file; do - FILES+=("$file") + FILES+=("$file") done < <(git ls-files -z "${EXCLUDE_PATTERNS[@]}") for file in "${FILES[@]}"; do - if ! check_or_fix_header "$file"; then - HAS_ERRORS=1 - fi + if ! check_or_fix_header "$file"; then + HAS_ERRORS=1 + fi done # Final result if [ "$HAS_ERRORS" -eq 1 ]; then - [ "$FIX_MODE" -eq 1 ] && log "⚠️ Some headers were fixed." || fatal "Some files have header issues." + if [ "$FIX_MODE" -eq 1 ]; then + log "⚠️ Some headers were fixed." + else + fatal "Some files have header issues." + fi else - [ "$FIX_MODE" -eq 1 ] && log "✅ Headers updated successfully." || log "✅ All headers are valid." -fi \ No newline at end of file + if [ "$FIX_MODE" -eq 1 ]; then + log "✅ Headers updated successfully." + else + log "✅ All headers are valid." + fi +fi diff --git a/scripts/check-swift-package.sh b/scripts/check-swift-package.sh index c398371..274d85a 100755 --- a/scripts/check-swift-package.sh +++ b/scripts/check-swift-package.sh @@ -6,19 +6,22 @@ set -u ERROR_COUNT=0 # Logging helpers -log() { printf -- "%s\n" "$*" >&2; } -error() { printf -- "%s\n" "$*" >&2; ERROR_COUNT=$((ERROR_COUNT + 1)); } +log() { printf -- "%s\n" "$*" >&2; } +error() { + printf -- "%s\n" "$*" >&2 + ERROR_COUNT=$((ERROR_COUNT + 1)) +} fatal() { - printf -- "%s\n" "$*" >&2 - exit 1 + printf -- "%s\n" "$*" >&2 + exit 1 } fatal_if_errors() { - if [ "$ERROR_COUNT" -gt 0 ]; then - printf -- "\n❌ %d error(s) found\n" "$ERROR_COUNT" >&2 - exit 1 - fi + if [ "$ERROR_COUNT" -gt 0 ]; then + printf -- "\n❌ %d error(s) found\n" "$ERROR_COUNT" >&2 + exit 1 + fi } log "Starting Swift Package validation" @@ -28,12 +31,12 @@ command -v jq >/dev/null 2>&1 || fatal "❌ jq not found (required)" # Helper to check required files check_file() { - file="$1" - if [ -f "$file" ]; then - log "✅ $file exists" - else - error "❌ $file missing (expected at repository root)" - fi + file="$1" + if [ -f "$file" ]; then + log "✅ $file exists" + else + error "❌ $file missing (expected at repository root)" + fi } # Package.swift @@ -42,65 +45,65 @@ check_file "Package.swift" # swift-tools-version TOOLS_LINE="$(grep '^// swift-tools-version:' Package.swift 2>/dev/null || true)" if [ -n "$TOOLS_LINE" ]; then - TOOLS_VERSION="$(printf "%s" "$TOOLS_LINE" | sed 's/.*: *//')" - major="$(printf "%s" "$TOOLS_VERSION" | cut -d. -f1)" - minor="$(printf "%s" "$TOOLS_VERSION" | cut -d. -f2)" - - if [ "$major" -lt 6 ] || { [ "$major" -eq 6 ] && [ "$minor" -lt 1 ]; }; then - error "❌ swift-tools-version too old ($TOOLS_VERSION), expected >= 6.1" - else - log "✅ swift-tools-version >= 6.1 ($TOOLS_VERSION)" - fi + TOOLS_VERSION="$(printf "%s" "$TOOLS_LINE" | sed 's/.*: *//')" + major="$(printf "%s" "$TOOLS_VERSION" | cut -d. -f1)" + minor="$(printf "%s" "$TOOLS_VERSION" | cut -d. -f2)" + + if [ "$major" -lt 6 ] || { [ "$major" -eq 6 ] && [ "$minor" -lt 1 ]; }; then + error "❌ swift-tools-version too old ($TOOLS_VERSION), expected >= 6.1" + else + log "✅ swift-tools-version >= 6.1 ($TOOLS_VERSION)" + fi else - error "❌ swift-tools-version missing" + error "❌ swift-tools-version missing" fi # defaultSwiftSettings if grep -q 'defaultSwiftSettings' Package.swift 2>/dev/null; then - log "✅ defaultSwiftSettings found" + log "✅ defaultSwiftSettings found" else - error "❌ defaultSwiftSettings missing" + error "❌ defaultSwiftSettings missing" fi # Swift 6 concurrency default (string presence only) if grep -q '"NonisolatedNonsendingByDefault"' Package.swift 2>/dev/null; then - log "✅ NonisolatedNonsendingByDefault present" + log "✅ NonisolatedNonsendingByDefault present" else - error "❌ NonisolatedNonsendingByDefault missing" + error "❌ NonisolatedNonsendingByDefault missing" fi # Parse Package.swift via SwiftPM PACKAGE_JSON="$(swift package dump-package 2>/dev/null || true)" if [ -n "$PACKAGE_JSON" ]; then - log "✅ Package.swift parsed by SwiftPM" + log "✅ Package.swift parsed by SwiftPM" else - error "❌ Failed to parse Package.swift via SwiftPM" + error "❌ Failed to parse Package.swift via SwiftPM" fi # Top-level dependencies if [ -n "$PACKAGE_JSON" ]; then - if echo "$PACKAGE_JSON" | jq -e '.dependencies | type == "array"' >/dev/null; then - log "✅ Top-level dependencies array exists" - else - error "❌ Top-level dependencies missing" - fi + if echo "$PACKAGE_JSON" | jq -e '.dependencies | type == "array"' >/dev/null; then + log "✅ Top-level dependencies array exists" + else + error "❌ Top-level dependencies missing" + fi fi # docc placeholder if grep -q '// *\[docc-plugin-placeholder\]' Package.swift 2>/dev/null; then - log "✅ docc plugin placeholder present" + log "✅ docc plugin placeholder present" else - error "❌ docc plugin placeholder missing" + error "❌ docc plugin placeholder missing" fi # swiftSettings: defaultSwiftSettings on all targets if [ -n "$PACKAGE_JSON" ]; then - TARGETS="$(echo "$PACKAGE_JSON" | jq -r '.targets[].name')" + TARGETS="$(echo "$PACKAGE_JSON" | jq -r '.targets[].name')" - for target in $TARGETS; do - if awk ' + for target in $TARGETS; do + if awk -v target="$target" ' # Start scanning when we see the target name - /name:[[:space:]]*"'$target'"/ { + $0 ~ "name:[[:space:]]*\"" target "\"" { in_target = 1 } @@ -122,26 +125,25 @@ if [ -n "$PACKAGE_JSON" ]; then END { exit !found } - ' Package.swift - then - log "✅ $target uses swiftSettings: defaultSwiftSettings" - else - error "❌ $target missing swiftSettings: defaultSwiftSettings" - fi - done + ' Package.swift; then + log "✅ $target uses swiftSettings: defaultSwiftSettings" + else + error "❌ $target missing swiftSettings: defaultSwiftSettings" + fi + done fi # Directories if [ -d Sources ]; then - log "✅ Sources directory exists" + log "✅ Sources directory exists" else - error "❌ Sources directory missing" + error "❌ Sources directory missing" fi if [ -d Tests ]; then - log "✅ Tests directory exists" + log "✅ Tests directory exists" else - error "❌ Tests directory missing" + error "❌ Tests directory missing" fi # Required repository files @@ -151,14 +153,13 @@ check_file "LICENSE" check_file "Makefile" check_file "README.md" - # LICENSE must contain current year CURRENT_YEAR="$(date +%Y)" if grep -q "$CURRENT_YEAR" LICENSE; then - log "✅ LICENSE contains current year ($CURRENT_YEAR)" + log "✅ LICENSE contains current year ($CURRENT_YEAR)" else - error "❌ LICENSE does not contain current year ($CURRENT_YEAR)" + error "❌ LICENSE does not contain current year ($CURRENT_YEAR)" fi fatal_if_errors diff --git a/scripts/check-unacceptable-language.sh b/scripts/check-unacceptable-language.sh index b7871a1..3cedd15 100755 --- a/scripts/check-unacceptable-language.sh +++ b/scripts/check-unacceptable-language.sh @@ -18,9 +18,12 @@ set -euo pipefail # Logging helpers # All output is written to stderr for consistent CI logs -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} # List of unacceptable or discouraged words # The list is converted into a single regex later @@ -28,6 +31,10 @@ UNACCEPTABLE_WORD_LIST="blacklist whitelist slave master sane sanity insane insa # Will contain matches if any unacceptable language is found PATHS_WITH_UNACCEPTABLE_LANGUAGE="" +UNACCEPTABLE_PATTERN="${UNACCEPTABLE_WORD_LIST// /|}" + +# Build git grep pathspec args. Start with repository root selector. +set -- . # If an ignore file exists, use it to exclude paths from the search # @@ -36,28 +43,24 @@ PATHS_WITH_UNACCEPTABLE_LANGUAGE="" if [[ -f .unacceptablelanguageignore ]]; then log "Found unacceptablelanguageignore file..." log "Checking for unacceptable language..." - - PATHS_WITH_UNACCEPTABLE_LANGUAGE=$( - tr '\n' '\0' < .unacceptablelanguageignore \ - | xargs -0 -I% printf '":(exclude)%" ' \ - | xargs git grep -i -I -w -H -n --column -E "${UNACCEPTABLE_WORD_LIST// /|}" \ - | grep -v "ignore-unacceptable-language" \ - || true - ) | /usr/bin/paste -s -d " " - + while IFS= read -r path; do + [ -z "${path}" ] && continue + set -- "$@" ":(exclude)${path}" + done <.unacceptablelanguageignore else log "Checking for unacceptable language..." - - PATHS_WITH_UNACCEPTABLE_LANGUAGE=$( - git grep -i -I -w -H -n --column -E "${UNACCEPTABLE_WORD_LIST// /|}" \ - | grep -v "ignore-unacceptable-language" \ - || true - ) | /usr/bin/paste -s -d " " - fi +PATHS_WITH_UNACCEPTABLE_LANGUAGE="$( + git grep -i -I -w -H -n --column -E "${UNACCEPTABLE_PATTERN}" -- "$@" | + grep -v "ignore-unacceptable-language" || + true +)" + # If any matches were found, fail the script and print the affected files if [ -n "${PATHS_WITH_UNACCEPTABLE_LANGUAGE}" ]; then fatal "❌ Found unacceptable language in files: ${PATHS_WITH_UNACCEPTABLE_LANGUAGE}." fi # Success -log "✅ Found no unacceptable language." \ No newline at end of file +log "✅ Found no unacceptable language." diff --git a/scripts/generate-contributors-list.sh b/scripts/generate-contributors-list.sh index b29d701..d725d75 100644 --- a/scripts/generate-contributors-list.sh +++ b/scripts/generate-contributors-list.sh @@ -18,9 +18,12 @@ set -euo pipefail # Logging helpers # All output is written to stderr for consistent CI and local logs -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} # Resolve the repository root # Ensures the CONTRIBUTORS.txt file is always written at the top level @@ -28,13 +31,14 @@ REPO_ROOT="$(git -C "$PWD" rev-parse --show-toplevel)" # Collect contributors from git history # -# - `git shortlog -es` groups commits by author +# - `git shortlog -es HEAD` groups commits by author # - Output format: "\tName " -contributors=$(git shortlog -es 2>/dev/null) - -# If git shortlog fails, the repository is likely invalid -if [ $? -ne 0 ]; then - fatal "Error: Unable to run 'git shortlog'. Are you in a valid git repository?" +if git rev-parse --verify HEAD >/dev/null 2>&1; then + if ! contributors=$(git shortlog -es HEAD 2>/dev/null); then + fatal "Error: Unable to run 'git shortlog'. Are you in a valid git repository?" + fi +else + contributors="" fi # Handle the case where no contributors are found @@ -44,7 +48,7 @@ if [ -z "$contributors" ]; then exit 0 else # Strip commit counts and format as a Markdown list - contributors=$(echo "$contributors" | awk '{$1=""; print "- " $0}') + contributors=$(printf '%s\n' "$contributors" | awk '{$1=""; print "- " $0}') fi log "Creating CONTRIBUTORS.txt file..." @@ -59,7 +63,7 @@ log "Creating CONTRIBUTORS.txt file..." # The .mailmap file should be used to: # - Fix misspelled names # - Merge duplicate identities -cat > "$REPO_ROOT/CONTRIBUTORS.txt" <<- EOF +cat >"$REPO_ROOT/CONTRIBUTORS.txt" <<-EOF ### Contributors $contributors @@ -69,4 +73,4 @@ cat > "$REPO_ROOT/CONTRIBUTORS.txt" <<- EOF EOF # Success message -log "✅ CONTRIBUTORS.txt created with no errors." \ No newline at end of file +log "✅ CONTRIBUTORS.txt created with no errors." diff --git a/scripts/generate-docc.sh b/scripts/generate-docc.sh index 75bc8ff..1edf8ff 100755 --- a/scripts/generate-docc.sh +++ b/scripts/generate-docc.sh @@ -18,9 +18,12 @@ set -euo pipefail # Logging helpers # All output is written to stderr for consistent CI and local logs -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} # Configuration / state OUTPUT_DIR="./docs" # Output directory for generated documentation @@ -34,9 +37,10 @@ REPO_NAME="" # Hosting base path (for GitHub Pages) # Swift package manifest to mutate PACKAGE_FILE="Package.swift" # Required injection anchor inside dependencies -INJECT_MARKER='// [docc-plugin-placeholder]' +INJECT_MARKER='// [docc-plugin-placeholder]' # Dependency line injected after the marker DOCC_DEP=' .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.4.0"),' +DOCC_PLUGIN_INJECTED=false # Argument parsing # @@ -128,13 +132,14 @@ ensure_docc_plugin() { END { if (!injected) exit 42 } - ' "$PACKAGE_FILE" > "$PACKAGE_FILE.tmp" + ' "$PACKAGE_FILE" >"$PACKAGE_FILE.tmp" mv "$PACKAGE_FILE.tmp" "$PACKAGE_FILE" + DOCC_PLUGIN_INJECTED=true # Validate manifest after mutation - swift package dump-package >/dev/null \ - || fatal "Package.swift became invalid after injecting swift-docc-plugin" + swift package dump-package >/dev/null || + fatal "Package.swift became invalid after injecting swift-docc-plugin" } # Restore git state after documentation generation (local only) @@ -151,6 +156,29 @@ reset_git_after_docs() { fi } +# shellcheck disable=SC2317,SC2329 +restore_injected_package_manifest() { + if [ "${DOCC_PLUGIN_INJECTED}" != "true" ]; then + return 0 + fi + + rm -f "$PACKAGE_FILE.tmp" + + if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + log "Restoring ${PACKAGE_FILE} after failed documentation generation" + git checkout -- "$PACKAGE_FILE" || true + fi +} + +# shellcheck disable=SC2317,SC2329 +cleanup_on_exit() { + local rc=$? + if [ "$rc" -ne 0 ]; then + restore_injected_package_manifest + fi +} +trap cleanup_on_exit EXIT + # Determine repository name # # Used as the hosting base path when generating documentation for @@ -186,7 +214,7 @@ load_from_config() { fi for TARGET in $TARGETS; do - TARGET_FLAGS+=( --target "$TARGET" ) + TARGET_FLAGS+=(--target "$TARGET") done } @@ -198,8 +226,8 @@ auto_detect_targets() { fatal "jq is required (install with: brew install jq)" fi - TARGETS=$(swift package dump-package \ - | jq -r '.targets[] + TARGETS=$(swift package dump-package | + jq -r '.targets[] | select(.type == "regular" or .type == "executable") | .name') @@ -208,7 +236,7 @@ auto_detect_targets() { fi for TARGET in $TARGETS; do - TARGET_FLAGS+=( --target "$TARGET" ) + TARGET_FLAGS+=(--target "$TARGET") done } @@ -233,7 +261,7 @@ generate_pages_redirects() { # ------------------------------------------------------------ # 1. Site root redirect → /documentation/ # ------------------------------------------------------------ - cat > "$OUTPUT_DIR/index.html" <"$OUTPUT_DIR/index.html" < @@ -259,13 +287,19 @@ EOF # 3. Single-target → redirect /documentation/ → /documentation// # ------------------------------------------------------------ local TARGET - TARGET=$(ls -d "$DOC_ROOT"/*/ 2>/dev/null | head -n 1 | xargs basename) + local FIRST_TARGET_DIR + FIRST_TARGET_DIR="$(find "$DOC_ROOT" -mindepth 1 -maxdepth 1 -type d -print -quit)" + if [ -n "$FIRST_TARGET_DIR" ]; then + TARGET="$(basename "$FIRST_TARGET_DIR")" + else + TARGET="" + fi if [ -z "$TARGET" ]; then fatal "Unable to determine single DocC target" fi - cat > "$DOC_ROOT/index.html" <"$DOC_ROOT/index.html" < @@ -323,8 +357,8 @@ if $LOCAL_MODE; then generate-documentation \ $COMBINED_FLAG \ "${TARGET_FLAGS[@]}" \ - --output-path "$OUTPUT_DIR" \ - || DOCS_EXIT_CODE=$? + --output-path "$OUTPUT_DIR" || + DOCS_EXIT_CODE=$? else swift package \ --allow-writing-to-directory "$OUTPUT_DIR" \ @@ -333,8 +367,8 @@ else "${TARGET_FLAGS[@]}" \ --output-path "$OUTPUT_DIR" \ --transform-for-static-hosting \ - ${REPO_NAME:+--hosting-base-path "$REPO_NAME"} \ - || DOCS_EXIT_CODE=$? + ${REPO_NAME:+--hosting-base-path "$REPO_NAME"} || + DOCS_EXIT_CODE=$? fi # Report failure without hiding the exit code @@ -348,4 +382,4 @@ fi # Cleanup and exit reset_git_after_docs -exit $DOCS_EXIT_CODE \ No newline at end of file +exit $DOCS_EXIT_CODE diff --git a/scripts/install-swift-openapi-generator.sh b/scripts/install-swift-openapi-generator.sh index 20938b0..0760771 100755 --- a/scripts/install-swift-openapi-generator.sh +++ b/scripts/install-swift-openapi-generator.sh @@ -18,38 +18,63 @@ set -euo pipefail # Logging helpers # All output is written to stderr for consistent CI and local logs -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} # Repository containing the Swift OpenAPI Generator REPO="https://github.com/apple/swift-openapi-generator" +VERSION="" -# Determine the latest available tag by default -# -# Tags are sorted using semantic version ordering to ensure -# the newest release is selected. -VERSION=$(git ls-remote --tags --sort="v:refname" "${REPO}" \ - | tail -n1 \ - | sed 's/.*\///; s/\^{}//') +usage() { + cat >&2 </dev/null 2>&1 || + fatal "'${required_cmd}' is required but not installed" +done + +if [ -z "${VERSION}" ]; then + # Determine latest available tag by default. + VERSION=$(git ls-remote --tags --sort="v:refname" "${REPO}" | + tail -n1 | + sed 's/.*\///; s/\^{}//') +fi + +[ -n "${VERSION}" ] || + fatal "Unable to resolve swift-openapi-generator version" + log "Installing swift-openapi-generator version: ${VERSION}" # Download the source archive for the selected version -curl -L -o "${VERSION}.tar.gz" \ +curl -fL -o "${VERSION}.tar.gz" \ "${REPO}/archive/refs/tags/${VERSION}.tar.gz" # Extract the source archive @@ -71,4 +96,4 @@ cd .. rm -f "${VERSION}.tar.gz" rm -rf "swift-openapi-generator-${VERSION}" -log "✅ swift-openapi-generator ${VERSION} installed successfully." \ No newline at end of file +log "✅ swift-openapi-generator ${VERSION} installed successfully." diff --git a/scripts/run-actionlint.sh b/scripts/run-actionlint.sh new file mode 100755 index 0000000..9737704 --- /dev/null +++ b/scripts/run-actionlint.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# GitHub Actions Workflow Lint Script +# +# Runs actionlint against repository workflows. + +set -euo pipefail + +log() { printf -- "** %s\n" "$*" >&2; } +error() { printf -- "** ERROR: %s\n" "$*" >&2; } +fatal() { + error "$@" + exit 1 +} + +if ! command -v actionlint >/dev/null 2>&1; then + fatal "actionlint is not installed. Install it first (e.g. 'brew install actionlint' or 'apt-get install actionlint')." +fi + +REPO_ROOT="$(git -C "$PWD" rev-parse --show-toplevel 2>/dev/null || pwd)" +cd "$REPO_ROOT" + +log "Running actionlint..." +actionlint "$@" +log "✅ actionlint found no issues." diff --git a/scripts/run-clean.sh b/scripts/run-clean.sh index bb38111..4cca511 100755 --- a/scripts/run-clean.sh +++ b/scripts/run-clean.sh @@ -17,13 +17,16 @@ set -euo pipefail # Logging helpers # All output is written to stderr for consistent logs -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} # Remove files and directories rm -rf ".build" rm -rf ".swiftpm" rm -f "openapi/openapi.yaml" rm -f "db.sqlite" -rm -f "migration-entries.json" \ No newline at end of file +rm -f "migration-entries.json" diff --git a/scripts/run-docc-docker.sh b/scripts/run-docc-docker.sh index f844aa0..4b9e46e 100755 --- a/scripts/run-docc-docker.sh +++ b/scripts/run-docc-docker.sh @@ -17,9 +17,12 @@ set -euo pipefail # Logging helpers # All output is written to stderr for consistent local logs -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} # Resolve repository root # Ensures the docs directory is located correctly even if the script @@ -70,4 +73,4 @@ echo docker run --rm --name "${NAME}" \ -v "${DOCC_DIR}:/usr/share/nginx/html" \ -p "${PORT}" \ - nginx \ No newline at end of file + nginx diff --git a/scripts/run-openapi-docker.sh b/scripts/run-openapi-docker.sh index 117260a..aac233e 100755 --- a/scripts/run-openapi-docker.sh +++ b/scripts/run-openapi-docker.sh @@ -16,24 +16,17 @@ set -euo pipefail # Logging helpers # All output is written to stderr for consistent local logs -log() { printf -- "** %s\n" "$*" >&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} -# Resolve repository root -# Allows the script to be executed from any subdirectory -REPO_ROOT="$(git -C "$PWD" rev-parse --show-toplevel)" - -# Location of OpenAPI files -# The directory is expected to contain one or more OpenAPI specifications -OPENAPI_YAML_LOCATION="${REPO_ROOT}/openapi" - -# If the OpenAPI directory does not exist, skip serving gracefully -# This avoids failing for repositories without API definitions -if [ ! -d "${OPENAPI_YAML_LOCATION}" ]; then - error "❗ OpenAPI location not found." - exit 0 -fi +# Resolve script directory +# Allows the script to be executed from any subdirectory. +SCRIPT_SOURCE="${BASH_SOURCE[0]-$0}" +SCRIPT_DIR="$(cd -- "$(dirname -- "${SCRIPT_SOURCE}")" && pwd)" # Default Docker container name NAME="openapi-server" @@ -42,24 +35,114 @@ NAME="openapi-server" # Nginx listens on port 80 inside the container PORT="8888:80" +# Default OpenAPI path (file or directory) +# If a file is provided, its parent directory will be mounted. +OPENAPI_PATH="openapi" + +resolve_repo_root() { + # Prefer git root for local execution and subdirectory calls. + if root="$(git rev-parse --show-toplevel 2>/dev/null)"; then + printf '%s\n' "${root}" + return 0 + fi + + # Fallback for checked-out scripts under a conventional ./scripts layout. + if [ -d "${SCRIPT_DIR}/../.git" ]; then + ( + cd -- "${SCRIPT_DIR}/.." + pwd + ) + return 0 + fi + + # Last resort for piped execution (for example: curl | bash). + pwd +} + +usage() { + cat >&2 <&2; } +log() { printf -- "** %s\n" "$*" >&2; } error() { printf -- "** ERROR: %s\n" "$*" >&2; } -fatal() { error "$@"; exit 1; } +fatal() { + error "$@" + exit 1 +} # Determine swift-format command mode # @@ -33,9 +36,9 @@ fatal() { error "$@"; exit 1; } # If --fix is provided, switch to in-place formatting. FORMAT_COMMAND=(lint --strict) for arg in "$@"; do - if [ "$arg" == "--fix" ]; then - FORMAT_COMMAND=(format --in-place) - fi + if [ "$arg" == "--fix" ]; then + FORMAT_COMMAND=(format --in-place) + fi done # Base URL for shared swift-format configuration files @@ -67,19 +70,19 @@ fi # - Runs formatting in parallel for performance # # The exit code is captured explicitly to allow controlled error handling. -tr '\n' '\0' < .swiftformatignore \ -| xargs -0 -I% printf '":(exclude)%" ' \ -| xargs git ls-files -z '*.swift' \ -| xargs -0 swift format "${FORMAT_COMMAND[@]}" --parallel \ -&& SWIFT_FORMAT_RC=$? || SWIFT_FORMAT_RC=$? +tr '\n' '\0' <.swiftformatignore | + xargs -0 -I% printf '":(exclude)%" ' | + xargs git ls-files -z '*.swift' | + xargs -0 swift format "${FORMAT_COMMAND[@]}" --parallel && + SWIFT_FORMAT_RC=$? || SWIFT_FORMAT_RC=$? # If swift-format failed, print a helpful error message if [ "${SWIFT_FORMAT_RC}" -ne 0 ]; then - fatal "❌ Running swift-format produced errors. + fatal "❌ Running swift-format produced errors. To fix: % run make format " fi # Success -log "✅ Ran swift-format with no errors." \ No newline at end of file +log "✅ Ran swift-format with no errors." diff --git a/scripts/script-format.sh b/scripts/script-format.sh new file mode 100755 index 0000000..a8fd365 --- /dev/null +++ b/scripts/script-format.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# Shell Script Format Check / Fix Script +# +# This script formats shell-related files with shfmt. +# +# Default behavior: +# - Runs shfmt in diff mode and fails when formatting drift is found. +# +# Optional behavior: +# - If --fix is passed, writes formatting changes in-place. + +set -euo pipefail + +log() { printf -- "** %s\n" "$*" >&2; } +error() { printf -- "** ERROR: %s\n" "$*" >&2; } +fatal() { + error "$@" + exit 1 +} + +usage() { + cat >&2 </dev/null 2>&1; then + fatal "shfmt is not installed. Install it first (e.g. 'brew install shfmt' or 'apt-get install shfmt')." +fi + +FILES=() +while IFS= read -r -d '' file; do + FILES+=("$file") +done < <(git ls-files -z '*.sh' '*.bash' '*.bats') + +if [ "${#FILES[@]}" -eq 0 ]; then + log "No shell files found to format." + exit 0 +fi + +if [ "$FIX_MODE" -eq 1 ]; then + shfmt -w -i 4 -ci "${FILES[@]}" + log "✅ Applied shfmt formatting." + exit 0 +fi + +set +e +shfmt -d -i 4 -ci "${FILES[@]}" +SHFMT_RC=$? +set -e + +if [ "$SHFMT_RC" -ne 0 ]; then + fatal "shfmt found formatting issues. Run with --fix to apply changes." +fi + +log "✅ shfmt found no formatting issues." diff --git a/tests/bats/check-broken-symlinks.bats b/tests/bats/check-broken-symlinks.bats new file mode 100755 index 0000000..1c20a11 --- /dev/null +++ b/tests/bats/check-broken-symlinks.bats @@ -0,0 +1,64 @@ +#!/usr/bin/env bats + +load 'helpers/test_helper.bash' + +@test "check-broken-symlinks passes when all symlinks are valid" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-broken-symlinks.sh" + + cat >"$repo/target.txt" <<'TXT' +hello +TXT + ln -s target.txt "$repo/link.txt" + commit_all "$repo" + + run bash -c "cd '$repo' && bash scripts/check-broken-symlinks.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Found 0 broken symlinks"* ]] +} + +@test "check-broken-symlinks fails when symlink target is missing" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-broken-symlinks.sh" + + ln -s missing.txt "$repo/broken.txt" + commit_all "$repo" + + run bash -c "cd '$repo' && bash scripts/check-broken-symlinks.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"Broken symlink: broken.txt"* ]] +} + +@test "check-broken-symlinks ignores regular files even if they mention missing paths" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-broken-symlinks.sh" + + cat >"$repo/notes.txt" <<'TXT' +This mentions a missing file path: ./does-not-exist.txt +TXT + commit_all "$repo" + + run bash -c "cd '$repo' && bash scripts/check-broken-symlinks.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Found 0 broken symlinks"* ]] +} + +@test "check-broken-symlinks checks only tracked symlinks" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-broken-symlinks.sh" + + cat >"$repo/tracked.txt" <<'TXT' +ok +TXT + git -C "$repo" add tracked.txt + git -C "$repo" commit -q -m "baseline" + ln -s missing.txt "$repo/untracked-broken.txt" + + run bash -c "cd '$repo' && bash scripts/check-broken-symlinks.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Found 0 broken symlinks"* ]] +} diff --git a/tests/bats/check-local-swift-dependencies.bats b/tests/bats/check-local-swift-dependencies.bats new file mode 100755 index 0000000..cf10dc9 --- /dev/null +++ b/tests/bats/check-local-swift-dependencies.bats @@ -0,0 +1,95 @@ +#!/usr/bin/env bats + +load 'helpers/test_helper.bash' + +@test "check-local-swift-dependencies passes when no .package(path:) is present" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-local-swift-dependencies.sh" + + cat >"$repo/Package.swift" <<'SWIFT' +// swift-tools-version: 6.1 +import PackageDescription + +let package = Package( + name: "Demo", + dependencies: [ + .package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0") + ], + targets: [] +) +SWIFT + commit_all "$repo" + + run bash -c "cd '$repo' && bash scripts/check-local-swift-dependencies.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Found 0 local Swift package dependency references"* ]] +} + +@test "check-local-swift-dependencies fails when .package(path:) exists" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-local-swift-dependencies.sh" + + cat >"$repo/Package.swift" <<'SWIFT' +// swift-tools-version: 6.1 +import PackageDescription + +let package = Package( + name: "Demo", + dependencies: [ + .package(path: "../LocalPackage") + ], + targets: [] +) +SWIFT + commit_all "$repo" + + run bash -c "cd '$repo' && bash scripts/check-local-swift-dependencies.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"contains local Swift package reference"* ]] +} + +@test "check-local-swift-dependencies ignores untracked Package.swift" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-local-swift-dependencies.sh" + + cat >"$repo/Package.swift" <<'SWIFT' +// swift-tools-version: 6.1 +import PackageDescription + +let package = Package( + name: "Demo", + dependencies: [ + .package(path: "../LocalPackage") + ], + targets: [] +) +SWIFT + cat >"$repo/README.md" <<'TXT' +fixture +TXT + git -C "$repo" add README.md + git -C "$repo" commit -q -m "tracked file only" + + run bash -c "cd '$repo' && bash scripts/check-local-swift-dependencies.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Found 0 local Swift package dependency references"* ]] +} + +@test "check-local-swift-dependencies passes when no Package.swift is tracked" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-local-swift-dependencies.sh" + + cat >"$repo/README.md" <<'TXT' +fixture +TXT + git -C "$repo" add README.md + git -C "$repo" commit -q -m "no package swift" + + run bash -c "cd '$repo' && bash scripts/check-local-swift-dependencies.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Found 0 local Swift package dependency references"* ]] +} diff --git a/tests/bats/check-swift-headers.bats b/tests/bats/check-swift-headers.bats new file mode 100755 index 0000000..362aa43 --- /dev/null +++ b/tests/bats/check-swift-headers.bats @@ -0,0 +1,294 @@ +#!/usr/bin/env bats + +load 'helpers/test_helper.bash' + +REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME}")/../.." && pwd)" + +setup() { + TMP_ROOT="$(mktemp -d)" + TMPDIR="$TMP_ROOT/HeaderProject" + mkdir -p "$TMPDIR" + + copy_script_into_repo "$TMPDIR" "check-swift-headers.sh" +} + +teardown() { + rm -rf "$TMP_ROOT" +} + +init_git() { + git -C "$TMPDIR" init -q + git -C "$TMPDIR" config user.name "Test User" + git -C "$TMPDIR" config user.email "test@example.com" + git -C "$TMPDIR" add -A + GIT_AUTHOR_DATE="2026-02-12T12:00:00Z" \ + GIT_COMMITTER_DATE="2026-02-12T12:00:00Z" \ + git -C "$TMPDIR" commit -q -m "init fixtures" +} + +run_checker() { + (cd "$TMPDIR" && bash scripts/check-swift-headers.sh "$@") +} + +@test "valid header with single dot passes without changes" { + cp "$REPO_ROOT/tests/fixtures/valid_single_dot.swift" \ + "$TMPDIR/Address.swift" + + init_git + + run run_checker + [ "$status" -eq 0 ] + + diff -u \ + "$REPO_ROOT/tests/fixtures/valid_single_dot.swift" \ + "$TMPDIR/Address.swift" +} + +@test "legacy double-dot header is normalized but not duplicated" { + cp "$REPO_ROOT/tests/fixtures/legacy_double_dot.swift" \ + "$TMPDIR/Address.swift" + + init_git + + run run_checker --fix + [ "$status" -eq 0 ] + + diff -u \ + "$REPO_ROOT/tests/fixtures/legacy_double_dot_fixed.swift" \ + "$TMPDIR/Address.swift" + + run run_checker + [ "$status" -eq 0 ] +} + +@test "wrong project name is fixed but author and date are preserved" { + cp "$REPO_ROOT/tests/fixtures/wrong_project.swift" \ + "$TMPDIR/WrongProject.swift" + + init_git + + run run_checker --fix + [ "$status" -eq 0 ] + + diff -u \ + "$REPO_ROOT/tests/fixtures/wrong_project_fixed.swift" \ + "$TMPDIR/WrongProject.swift" + + run run_checker + [ "$status" -eq 0 ] +} + +@test "wrong project name fails in check mode" { + cp "$REPO_ROOT/tests/fixtures/wrong_project.swift" \ + "$TMPDIR/WrongProject.swift" + + init_git + + run run_checker + [ "$status" -ne 0 ] + [[ "$output" == *"Header is invalid"* ]] +} + +@test "missing header fails in check mode" { + cp "$REPO_ROOT/tests/fixtures/missing_header.swift" \ + "$TMPDIR/Foo.swift" + + init_git + + run run_checker + [ "$status" -ne 0 ] +} + +@test "missing header with leading random comments fails in check mode" { + cat >"$TMPDIR/Foo.swift" <<'EOF2' +// random comment one +// random comment two +import Foundation + +struct Foo { + let value: Int +} +EOF2 + + init_git + + run run_checker + [ "$status" -ne 0 ] + [[ "$output" == *"Header missing"* ]] +} + +@test "missing header is inserted in fix mode" { + cp "$REPO_ROOT/tests/fixtures/missing_header.swift" \ + "$TMPDIR/Foo.swift" + + init_git + + run run_checker --fix + [ "$status" -eq 0 ] + + diff -u \ + "$REPO_ROOT/tests/fixtures/missing_header_fixed.swift" \ + "$TMPDIR/Foo.swift" + + run run_checker + [ "$status" -eq 0 ] +} + +@test "missing header with leading random comments is fixed in fix mode" { + cat >"$TMPDIR/Foo.swift" <<'EOF2' +// random comment one +// random comment two +import Foundation + +struct Foo { + let value: Int +} +EOF2 + + init_git + + run run_checker --fix + [ "$status" -eq 0 ] + + content="$(cat "$TMPDIR/Foo.swift")" + [[ "$content" == *"// Foo.swift"* ]] + [[ "$content" == *"// Created by Binary Birds on"* ]] + [[ "$content" == *"// random comment one"* ]] + [[ "$content" == *"// random comment two"* ]] + + run run_checker + [ "$status" -eq 0 ] +} + +@test "fix mode is idempotent" { + cp "$REPO_ROOT/tests/fixtures/legacy_double_dot.swift" \ + "$TMPDIR/Address.swift" + + init_git + + run run_checker --fix + [ "$status" -eq 0 ] + + cp "$TMPDIR/Address.swift" "$TMPDIR/once.swift" + + run run_checker --fix + [ "$status" -eq 0 ] + + diff -u "$TMPDIR/once.swift" "$TMPDIR/Address.swift" +} + +@test "--author without value fails with clear error" { + cp "$REPO_ROOT/tests/fixtures/missing_header.swift" \ + "$TMPDIR/Foo.swift" + + init_git + + run run_checker --fix --author + [ "$status" -ne 0 ] + [[ "$output" == *"--author requires a value"* ]] +} + +@test "unknown argument fails with clear error" { + cp "$REPO_ROOT/tests/fixtures/missing_header.swift" \ + "$TMPDIR/Foo.swift" + + init_git + + run run_checker --unknown + [ "$status" -ne 0 ] + [[ "$output" == *"Unknown argument: --unknown"* ]] +} + +@test "Package.swift is skipped even without header" { + cat >"$TMPDIR/Package.swift" <<'EOF2' +// swift-tools-version: 6.1 +import PackageDescription + +let package = Package( + name: "HeaderProject", + targets: [] +) +EOF2 + cp "$REPO_ROOT/tests/fixtures/valid_single_dot.swift" \ + "$TMPDIR/Address.swift" + + init_git + + run run_checker + [ "$status" -eq 0 ] +} + +@test "invalid header fails in check mode" { + cat >"$TMPDIR/Bad.swift" <<'EOF2' +// +// WrongName.swift +// HeaderProject +// +// Created by Test User on 2026. 02. 12. + +struct Bad {} +EOF2 + + init_git + + run run_checker + [ "$status" -ne 0 ] + [[ "$output" == *"Header is invalid"* ]] +} + +@test "invalid header is fixed while preserving author and date" { + cat >"$TMPDIR/Preserve.swift" <<'EOF2' +// +// WrongName.swift +// WrongProject +// +// Created by Alice Doe on 2026. 02. 12. + +struct Preserve {} +EOF2 + + init_git + + run run_checker --fix + [ "$status" -eq 0 ] + + content="$(cat "$TMPDIR/Preserve.swift")" + [[ "$content" == *"// Preserve.swift"* ]] + [[ "$content" == *"// HeaderProject"* ]] + [[ "$content" == *"// Created by Alice Doe on 2026. 02. 12."* ]] + + run run_checker + [ "$status" -eq 0 ] +} + +@test ".swiftheaderignore supports comments and blank lines" { + cp "$REPO_ROOT/tests/fixtures/missing_header.swift" \ + "$TMPDIR/Ignored.swift" + cp "$REPO_ROOT/tests/fixtures/valid_single_dot.swift" \ + "$TMPDIR/Address.swift" + cat >"$TMPDIR/.swiftheaderignore" <<'EOF2' +# Ignore this swift file +Ignored.swift + +# Also ignore helper shell scripts +scripts/** +.swiftheaderignore +EOF2 + + init_git + + run run_checker + [ "$status" -eq 0 ] +} + +@test "running from subdirectory behaves the same" { + mkdir -p "$TMPDIR/Sources" + cp "$REPO_ROOT/tests/fixtures/missing_header.swift" \ + "$TMPDIR/Sources/Foo.swift" + + init_git + + run bash -c "cd '$TMPDIR/Sources' && ../scripts/check-swift-headers.sh" + [ "$status" -ne 0 ] + [[ "$output" == *"Header missing"* ]] +} diff --git a/tests/bats/check-swift-package.bats b/tests/bats/check-swift-package.bats new file mode 100755 index 0000000..029a9e6 --- /dev/null +++ b/tests/bats/check-swift-package.bats @@ -0,0 +1,264 @@ +#!/usr/bin/env bats + +load 'helpers/test_helper.bash' + +setup_valid_swift_package_layout() { + local repo="$1" + local year + year="$(date +%Y)" + + mkdir -p "$repo/Sources/Demo" "$repo/Tests/DemoTests" + cat >"$repo/.swift-format" <<'EOF' +{} +EOF + : >"$repo/.swiftformatignore" + cat >"$repo/Makefile" <<'EOF' +all: + @echo ok +EOF + cat >"$repo/README.md" <<'EOF' +# Demo +EOF + cat >"$repo/LICENSE" <"$repo/Package.swift" <<'EOF' +// swift-tools-version: 6.1 +import PackageDescription + +let defaultSwiftSettings: [SwiftSetting] = [ + .enableUpcomingFeature("NonisolatedNonsendingByDefault") +] + +let package = Package( + name: "Demo", + products: [ + .library(name: "Demo", targets: ["Demo"]) + ], + dependencies: [ + // [docc-plugin-placeholder] + ], + targets: [ + .target( + name: "Demo", + swiftSettings: defaultSwiftSettings + ), + .testTarget( + name: "DemoTests", + dependencies: ["Demo"], + swiftSettings: defaultSwiftSettings + ) + ] +) +EOF +} + +install_swift_dump_stub_success() { + local repo="$1" + + mkdir -p "$repo/fakebin" + cat >"$repo/fakebin/swift" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +if [ "${1:-}" = "package" ] && [ "${2:-}" = "dump-package" ]; then + cat <<'JSON' +{"dependencies":[],"targets":[{"name":"Demo"},{"name":"DemoTests"}]} +JSON + exit 0 +fi +exit 1 +EOF + chmod +x "$repo/fakebin/swift" + export PATH="$repo/fakebin:$PATH" +} + +install_swift_dump_stub_failure() { + local repo="$1" + + mkdir -p "$repo/fakebin" + cat >"$repo/fakebin/swift" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +if [ "${1:-}" = "package" ] && [ "${2:-}" = "dump-package" ]; then + exit 1 +fi +exit 1 +EOF + chmod +x "$repo/fakebin/swift" + export PATH="$repo/fakebin:$PATH" +} + +@test "check-swift-package fails clearly when Package.swift is missing" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-swift-package.sh" + + run bash -c "cd '$repo' && sh scripts/check-swift-package.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"Package.swift missing"* ]] +} + +@test "check-swift-package passes on valid package structure and settings" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-swift-package.sh" + setup_valid_swift_package_layout "$repo" + write_valid_package_swift "$repo" + install_swift_dump_stub_success "$repo" + + run bash -c "cd '$repo' && sh scripts/check-swift-package.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Swift Package validation passed"* ]] +} + +@test "check-swift-package fails when swift-tools-version is below 6.1" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-swift-package.sh" + setup_valid_swift_package_layout "$repo" + write_valid_package_swift "$repo" + install_swift_dump_stub_success "$repo" + + sed -i.bak 's|// swift-tools-version: 6.1|// swift-tools-version: 5.9|' "$repo/Package.swift" + rm -f "$repo/Package.swift.bak" + + run bash -c "cd '$repo' && sh scripts/check-swift-package.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"swift-tools-version too old"* ]] +} + +@test "check-swift-package fails when docc placeholder is missing" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-swift-package.sh" + setup_valid_swift_package_layout "$repo" + write_valid_package_swift "$repo" + install_swift_dump_stub_success "$repo" + + sed -i.bak '/docc-plugin-placeholder/d' "$repo/Package.swift" + rm -f "$repo/Package.swift.bak" + + run bash -c "cd '$repo' && sh scripts/check-swift-package.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"docc plugin placeholder missing"* ]] +} + +@test "check-swift-package fails when a target misses swiftSettings: defaultSwiftSettings" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-swift-package.sh" + setup_valid_swift_package_layout "$repo" + cat >"$repo/Package.swift" <<'EOF' +// swift-tools-version: 6.1 +import PackageDescription + +let defaultSwiftSettings: [SwiftSetting] = [ + .enableUpcomingFeature("NonisolatedNonsendingByDefault") +] + +let package = Package( + name: "Demo", + products: [ + .library(name: "Demo", targets: ["Demo"]) + ], + dependencies: [ + // [docc-plugin-placeholder] + ], + targets: [ + .target( + name: "Demo", + swiftSettings: defaultSwiftSettings + ), + .testTarget( + name: "DemoTests", + dependencies: ["Demo"] + ) + ] +) +EOF + install_swift_dump_stub_success "$repo" + + run bash -c "cd '$repo' && sh scripts/check-swift-package.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"DemoTests missing swiftSettings: defaultSwiftSettings"* ]] +} + +@test "check-swift-package fails when LICENSE does not contain current year" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-swift-package.sh" + setup_valid_swift_package_layout "$repo" + write_valid_package_swift "$repo" + install_swift_dump_stub_success "$repo" + + cat >"$repo/LICENSE" <<'EOF' +Copyright 2001 +EOF + + run bash -c "cd '$repo' && sh scripts/check-swift-package.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"LICENSE does not contain current year"* ]] +} + +@test "check-swift-package fails when Sources directory is missing" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-swift-package.sh" + setup_valid_swift_package_layout "$repo" + write_valid_package_swift "$repo" + install_swift_dump_stub_success "$repo" + + rm -rf "$repo/Sources" + + run bash -c "cd '$repo' && sh scripts/check-swift-package.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"Sources directory missing"* ]] +} + +@test "check-swift-package fails when Tests directory is missing" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-swift-package.sh" + setup_valid_swift_package_layout "$repo" + write_valid_package_swift "$repo" + install_swift_dump_stub_success "$repo" + + rm -rf "$repo/Tests" + + run bash -c "cd '$repo' && sh scripts/check-swift-package.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"Tests directory missing"* ]] +} + +@test "check-swift-package fails when swift package dump-package fails" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-swift-package.sh" + setup_valid_swift_package_layout "$repo" + write_valid_package_swift "$repo" + install_swift_dump_stub_failure "$repo" + + run bash -c "cd '$repo' && sh scripts/check-swift-package.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"Failed to parse Package.swift via SwiftPM"* ]] +} + +@test "check-swift-package fails when NonisolatedNonsendingByDefault is missing" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-swift-package.sh" + setup_valid_swift_package_layout "$repo" + write_valid_package_swift "$repo" + install_swift_dump_stub_success "$repo" + + sed -i.bak '/NonisolatedNonsendingByDefault/d' "$repo/Package.swift" + rm -f "$repo/Package.swift.bak" + + run bash -c "cd '$repo' && sh scripts/check-swift-package.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"NonisolatedNonsendingByDefault missing"* ]] +} diff --git a/tests/bats/check-unacceptable-language.bats b/tests/bats/check-unacceptable-language.bats new file mode 100755 index 0000000..5d74e4c --- /dev/null +++ b/tests/bats/check-unacceptable-language.bats @@ -0,0 +1,109 @@ +#!/usr/bin/env bats + +load 'helpers/test_helper.bash' + +@test "check-unacceptable-language passes when no banned terms exist" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-unacceptable-language.sh" + + cat >"$repo/good.txt" <<'TXT' +This file uses inclusive and neutral wording. +TXT + commit_all "$repo" + + cat >"$repo/.unacceptablelanguageignore" <<'TXT' +scripts/check-unacceptable-language.sh +TXT + commit_all "$repo" "add ignore file" + + run bash -c "cd '$repo' && bash scripts/check-unacceptable-language.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Found no unacceptable language"* ]] +} + +@test "check-unacceptable-language fails when banned terms exist" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-unacceptable-language.sh" + + cat >"$repo/bad.txt" <<'TXT' +This line contains blacklist and should fail. +TXT + commit_all "$repo" + + run bash -c "cd '$repo' && bash scripts/check-unacceptable-language.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"Found unacceptable language"* ]] + [[ "$output" == *"bad.txt"* ]] +} + +@test "check-unacceptable-language respects .unacceptablelanguageignore" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-unacceptable-language.sh" + + cat >"$repo/bad.txt" <<'TXT' +This line contains blacklist. +TXT + cat >"$repo/.unacceptablelanguageignore" <<'TXT' +bad.txt +scripts/check-unacceptable-language.sh +TXT + commit_all "$repo" + + run bash -c "cd '$repo' && bash scripts/check-unacceptable-language.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Found no unacceptable language"* ]] +} + +@test "check-unacceptable-language ignores lines with ignore-unacceptable-language marker" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-unacceptable-language.sh" + + cat >"$repo/allowed.txt" <<'TXT' +blacklist // ignore-unacceptable-language +TXT + cat >"$repo/.unacceptablelanguageignore" <<'TXT' +scripts/check-unacceptable-language.sh +TXT + commit_all "$repo" + + run bash -c "cd '$repo' && bash scripts/check-unacceptable-language.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Found no unacceptable language"* ]] +} + +@test "check-unacceptable-language handles an empty ignore file" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-unacceptable-language.sh" + + cat >"$repo/good.txt" <<'TXT' +Inclusive wording only. +TXT + : >"$repo/.unacceptablelanguageignore" + git -C "$repo" add good.txt .unacceptablelanguageignore + git -C "$repo" commit -q -m "add fixtures" + + run bash -c "cd '$repo' && bash scripts/check-unacceptable-language.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Found no unacceptable language"* ]] +} + +@test "check-unacceptable-language matches whole words only" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-unacceptable-language.sh" + + cat >"$repo/words.txt" <<'TXT' +mastery and blacklisting should not match whole-word checks. +TXT + git -C "$repo" add words.txt + git -C "$repo" commit -q -m "add words fixture" + + run bash -c "cd '$repo' && bash scripts/check-unacceptable-language.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"Found no unacceptable language"* ]] +} diff --git a/tests/bats/docc-scripts.bats b/tests/bats/docc-scripts.bats new file mode 100644 index 0000000..5df3a3f --- /dev/null +++ b/tests/bats/docc-scripts.bats @@ -0,0 +1,95 @@ +#!/usr/bin/env bats + +load 'helpers/test_helper.bash' + +@test "check-docc-warnings restores Package.swift when analysis fails after plugin injection" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "check-docc-warnings.sh" + + cat >"$repo/Package.swift" <<'SWIFT' +// swift-tools-version: 6.1 +import PackageDescription + +let package = Package( + name: "DocCTest", + dependencies: [ + // [docc-plugin-placeholder] + ], + targets: [ + .target(name: "DocCTest") + ] +) +SWIFT + echo "DocCTest" >"$repo/.docctargetlist" + commit_all "$repo" + + mkdir -p "$repo/fakebin" + cat >"$repo/fakebin/swift" <<'STUB' +#!/usr/bin/env bash +set -euo pipefail +if [[ "$*" == "package dump-package" ]]; then + cat <<'JSON' +{"targets":[{"type":"regular","name":"DocCTest"}]} +JSON + exit 0 +fi +if [[ "$*" == *"plugin generate-documentation"* ]]; then + exit 1 +fi +exit 0 +STUB + chmod +x "$repo/fakebin/swift" + export PATH="$repo/fakebin:$PATH" + + run bash -c "cd '$repo' && bash scripts/check-docc-warnings.sh" + + [ "$status" -eq 1 ] + run bash -c "grep -q 'swift-docc-plugin' '$repo/Package.swift'" + [ "$status" -eq 1 ] +} + +@test "generate-docc restores Package.swift when generation fails after plugin injection" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "generate-docc.sh" + + cat >"$repo/Package.swift" <<'SWIFT' +// swift-tools-version: 6.1 +import PackageDescription + +let package = Package( + name: "DocCTest", + dependencies: [ + // [docc-plugin-placeholder] + ], + targets: [ + .target(name: "DocCTest") + ] +) +SWIFT + echo "DocCTest" >"$repo/.docctargetlist" + commit_all "$repo" + + mkdir -p "$repo/fakebin" + cat >"$repo/fakebin/swift" <<'STUB' +#!/usr/bin/env bash +set -euo pipefail +if [[ "$*" == "package dump-package" ]]; then + cat <<'JSON' +{"targets":[{"type":"regular","name":"DocCTest"}]} +JSON + exit 0 +fi +if [[ "$*" == *"generate-documentation"* ]]; then + exit 1 +fi +exit 0 +STUB + chmod +x "$repo/fakebin/swift" + export PATH="$repo/fakebin:$PATH" + + run bash -c "cd '$repo' && bash scripts/generate-docc.sh" + + [ "$status" -eq 1 ] + run bash -c "grep -q 'swift-docc-plugin' '$repo/Package.swift'" + [ "$status" -eq 1 ] +} diff --git a/tests/bats/generate-contributors-list.bats b/tests/bats/generate-contributors-list.bats new file mode 100755 index 0000000..dab7716 --- /dev/null +++ b/tests/bats/generate-contributors-list.bats @@ -0,0 +1,65 @@ +#!/usr/bin/env bats + +load 'helpers/test_helper.bash' + +@test "generate-contributors-list creates CONTRIBUTORS.txt from git history" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "generate-contributors-list.sh" + + cat >"$repo/file.txt" <<'TXT' +content +TXT + commit_all "$repo" + + run bash -c "cd '$repo' && bash scripts/generate-contributors-list.sh" + + [ "$status" -eq 0 ] + [ -f "$repo/CONTRIBUTORS.txt" ] + + contributors_content="$(cat "$repo/CONTRIBUTORS.txt")" + [[ "$contributors_content" == *"### Contributors"* ]] + [[ "$contributors_content" == *"Test User "* ]] +} + +@test "generate-contributors-list exits cleanly in repository with no commits" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "generate-contributors-list.sh" + + run bash -c "cd '$repo' && bash scripts/generate-contributors-list.sh" + + [ "$status" -eq 0 ] + [[ "$output" == *"No contributors found"* ]] + [ ! -f "$repo/CONTRIBUTORS.txt" ] +} + +@test "generate-contributors-list writes CONTRIBUTORS.txt at repo root from subdirectory" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "generate-contributors-list.sh" + mkdir -p "$repo/subdir" + + cat >"$repo/file.txt" <<'TXT' +content +TXT + commit_all "$repo" + + run bash -c "cd '$repo/subdir' && bash ../scripts/generate-contributors-list.sh" + + [ "$status" -eq 0 ] + [ -f "$repo/CONTRIBUTORS.txt" ] +} + +@test "generate-contributors-list output includes mailmap guidance" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "generate-contributors-list.sh" + + cat >"$repo/file.txt" <<'TXT' +content +TXT + commit_all "$repo" + + run bash -c "cd '$repo' && bash scripts/generate-contributors-list.sh" + + [ "$status" -eq 0 ] + contributors_content="$(cat "$repo/CONTRIBUTORS.txt")" + [[ "$contributors_content" == *"./.mailmap"* ]] +} diff --git a/tests/bats/helpers/test_helper.bash b/tests/bats/helpers/test_helper.bash new file mode 100755 index 0000000..5293b8d --- /dev/null +++ b/tests/bats/helpers/test_helper.bash @@ -0,0 +1,54 @@ +#!/usr/bin/env bash + +PROJECT_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME}")/../.." && pwd)" + +make_temp_repo() { + local repo + repo="$(mktemp -d)" + + git -C "$repo" init -q + git -C "$repo" config user.name "Test User" + git -C "$repo" config user.email "test@example.com" + + printf '%s\n' "$repo" +} + +copy_script_into_repo() { + local repo="$1" + local script_name="$2" + + mkdir -p "$repo/scripts" + cp "$PROJECT_ROOT/scripts/$script_name" "$repo/scripts/$script_name" + chmod +x "$repo/scripts/$script_name" +} + +copy_openapi_scripts_into_repo() { + local repo="$1" + copy_script_into_repo "$repo" "check-openapi-validation.sh" + copy_script_into_repo "$repo" "check-openapi-security.sh" + copy_script_into_repo "$repo" "run-openapi-docker.sh" +} + +install_docker_stub() { + local repo="$1" + + mkdir -p "$repo/fakebin" + cat >"$repo/fakebin/docker" <<'STUB' +#!/usr/bin/env bash +set -euo pipefail +: "${DOCKER_LOG:?DOCKER_LOG is required}" +printf '%s\n' "$*" >> "$DOCKER_LOG" +exit "${DOCKER_EXIT_CODE:-0}" +STUB + chmod +x "$repo/fakebin/docker" + + export PATH="$repo/fakebin:$PATH" +} + +commit_all() { + local repo="$1" + local message="${2:-test commit}" + + git -C "$repo" add -A + git -C "$repo" commit -q -m "$message" +} diff --git a/tests/bats/install-swift-openapi-generator.bats b/tests/bats/install-swift-openapi-generator.bats new file mode 100644 index 0000000..d5ec76f --- /dev/null +++ b/tests/bats/install-swift-openapi-generator.bats @@ -0,0 +1,89 @@ +#!/usr/bin/env bats + +load 'helpers/test_helper.bash' + +@test "install-swift-openapi-generator fails when latest version cannot be resolved" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "install-swift-openapi-generator.sh" + + mkdir -p "$repo/fakebin" + cat >"$repo/fakebin/git" <<'STUB' +#!/usr/bin/env bash +set -euo pipefail +if [[ "${1:-}" == "ls-remote" ]]; then + exit 0 +fi +exit 0 +STUB + cat >"$repo/fakebin/curl" <<'STUB' +#!/usr/bin/env bash +set -euo pipefail +exit 0 +STUB + cat >"$repo/fakebin/tar" <<'STUB' +#!/usr/bin/env bash +set -euo pipefail +exit 0 +STUB + cat >"$repo/fakebin/swift" <<'STUB' +#!/usr/bin/env bash +set -euo pipefail +exit 0 +STUB + cat >"$repo/fakebin/install" <<'STUB' +#!/usr/bin/env bash +set -euo pipefail +exit 0 +STUB + chmod +x "$repo/fakebin/git" "$repo/fakebin/curl" "$repo/fakebin/tar" "$repo/fakebin/swift" "$repo/fakebin/install" + export PATH="$repo/fakebin:$PATH" + + run bash -c "cd '$repo' && bash scripts/install-swift-openapi-generator.sh" + + [ "$status" -eq 1 ] + [[ "$output" == *"Unable to resolve swift-openapi-generator version"* ]] +} + +@test "install-swift-openapi-generator uses fail-fast curl flags" { + repo="$(make_temp_repo)" + copy_script_into_repo "$repo" "install-swift-openapi-generator.sh" + + mkdir -p "$repo/fakebin" + cat >"$repo/fakebin/git" <<'STUB' +#!/usr/bin/env bash +set -euo pipefail +exit 0 +STUB + cat >"$repo/fakebin/curl" <<'STUB' +#!/usr/bin/env bash +set -euo pipefail +: "${CURL_LOG:?CURL_LOG is required}" +printf '%s\n' "$*" >> "$CURL_LOG" +exit 0 +STUB + cat >"$repo/fakebin/tar" <<'STUB' +#!/usr/bin/env bash +set -euo pipefail +mkdir -p "swift-openapi-generator-v1.2.3" +exit 0 +STUB + cat >"$repo/fakebin/swift" <<'STUB' +#!/usr/bin/env bash +set -euo pipefail +exit 0 +STUB + cat >"$repo/fakebin/install" <<'STUB' +#!/usr/bin/env bash +set -euo pipefail +exit 0 +STUB + chmod +x "$repo/fakebin/git" "$repo/fakebin/curl" "$repo/fakebin/tar" "$repo/fakebin/swift" "$repo/fakebin/install" + export PATH="$repo/fakebin:$PATH" + export CURL_LOG="$repo/curl.log" + + run bash -c "cd '$repo' && bash scripts/install-swift-openapi-generator.sh -v v1.2.3" + + [ "$status" -eq 0 ] + curl_call="$(cat "$CURL_LOG")" + [[ "$curl_call" == *"-fL -o v1.2.3.tar.gz"* ]] +} diff --git a/tests/bats/openapi-scripts.bats b/tests/bats/openapi-scripts.bats new file mode 100755 index 0000000..95dc52d --- /dev/null +++ b/tests/bats/openapi-scripts.bats @@ -0,0 +1,475 @@ +#!/usr/bin/env bats + +load 'helpers/test_helper.bash' + +@test "check-openapi-validation resolves .yml to .yaml and calls docker with expected mount" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/api" + cat >"$repo/api/openapi.yaml" <<'YAML' +openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: {} +YAML + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && bash scripts/check-openapi-validation.sh -f '$repo/api/openapi.yml'" + + [ "$status" -eq 0 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"-v $repo/api/openapi.yaml:/openapi.yaml"* ]] + [[ "$docker_call" == *"pythonopenapi/openapi-spec-validator /openapi.yaml"* ]] +} + +@test "check-openapi-validation supports debug mode" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/api" + cat >"$repo/api/openapi.yaml" <<'YAML' +openapi: 3.0.0 +info: + title: Debug API + version: 1.0.0 +paths: {} +YAML + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && bash scripts/check-openapi-validation.sh -d -f '$repo/api/openapi.yaml'" + + [ "$status" -eq 0 ] + [[ "$output" == *"Debug enabled."* ]] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"-v $repo/api/openapi.yaml:/openapi.yaml"* ]] +} + +@test "check-openapi-validation --detailed runs Spectral diagnostics after validator failure" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + + mkdir -p "$repo/fakebin" + cat >"$repo/fakebin/docker" <<'STUB' +#!/usr/bin/env bash +set -euo pipefail +: "${DOCKER_LOG:?DOCKER_LOG is required}" +printf '%s\n' "$*" >> "$DOCKER_LOG" +if [[ "$*" == *"pythonopenapi/openapi-spec-validator"* ]]; then + exit 1 +fi +exit 0 +STUB + chmod +x "$repo/fakebin/docker" + export PATH="$repo/fakebin:$PATH" + + mkdir -p "$repo/api" + cat >"$repo/api/openapi.yaml" <<'YAML' +openapi: 3.0.0 +info: + title: Detailed API + version: 1.0.0 +paths: {} +YAML + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && bash scripts/check-openapi-validation.sh --detailed -f '$repo/api/openapi.yaml'" + + [ "$status" -eq 1 ] + docker_calls="$(cat "$DOCKER_LOG")" + [[ "$docker_calls" == *"pythonopenapi/openapi-spec-validator /openapi.yaml"* ]] + [[ "$docker_calls" == *"stoplight/spectral:latest lint /openapi.yaml"* ]] +} + +@test "check-openapi-validation resolves .yaml to .json and calls docker with expected mount" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/api" + cat >"$repo/api/openapi.json" <<'JSON' +{"openapi":"3.0.0","info":{"title":"JSON API","version":"1.0.0"},"paths":{}} +JSON + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && bash scripts/check-openapi-validation.sh -f '$repo/api/openapi.yaml'" + + [ "$status" -eq 0 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"-v $repo/api/openapi.json:/openapi.json"* ]] + [[ "$docker_call" == *"pythonopenapi/openapi-spec-validator /openapi.json"* ]] +} + +@test "check-openapi-validation accepts direct .yml input" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/api" + cat >"$repo/api/openapi.yml" <<'YAML' +openapi: 3.0.0 +info: + title: YML API + version: 1.0.0 +paths: {} +YAML + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && bash scripts/check-openapi-validation.sh -f '$repo/api/openapi.yml'" + + [ "$status" -eq 0 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"-v $repo/api/openapi.yml:/openapi.yml"* ]] + [[ "$docker_call" == *"pythonopenapi/openapi-spec-validator /openapi.yml"* ]] +} + +@test "check-openapi-validation accepts direct .json input" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/api" + cat >"$repo/api/openapi.json" <<'JSON' +{"openapi":"3.0.0","info":{"title":"Direct JSON API","version":"1.0.0"},"paths":{}} +JSON + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && bash scripts/check-openapi-validation.sh -f '$repo/api/openapi.json'" + + [ "$status" -eq 0 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"-v $repo/api/openapi.json:/openapi.json"* ]] + [[ "$docker_call" == *"pythonopenapi/openapi-spec-validator /openapi.json"* ]] +} + +@test "check-openapi-security exits successfully when OpenAPI path is missing" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && bash scripts/check-openapi-security.sh -f '$repo/does-not-exist'" + + [ "$status" -eq 0 ] + [[ "$output" == *"skipping security lint"* ]] + [ ! -f "$DOCKER_LOG" ] +} + +@test "check-openapi-security resolves .yml to .json and calls docker with expected mount" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/spec" + cat >"$repo/spec/openapi.json" <<'JSON' +{"openapi":"3.0.0","info":{"title":"Security JSON API","version":"1.0.0"},"paths":{}} +JSON + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && bash scripts/check-openapi-security.sh -f '$repo/spec/openapi.yml'" + + [ "$status" -eq 0 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"-v $repo/spec/openapi.json:/app/openapi.json"* ]] + [[ "$docker_call" == *"stoplight/spectral:latest lint /app/openapi.json"* ]] + [[ "$docker_call" == *"--ruleset /tmp/spectral-security-ruleset.yaml"* ]] +} + +@test "check-openapi-security accepts direct .yml input" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/spec" + cat >"$repo/spec/openapi.yml" <<'YAML' +openapi: 3.0.0 +info: + title: Security YML API + version: 1.0.0 +paths: {} +YAML + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && bash scripts/check-openapi-security.sh -f '$repo/spec/openapi.yml'" + + [ "$status" -eq 0 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"-v $repo/spec/openapi.yml:/app/openapi.yml"* ]] + [[ "$docker_call" == *"stoplight/spectral:latest lint /app/openapi.yml"* ]] +} + +@test "check-openapi-security with directory uses openapi.json when yaml/yml are absent" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/openapi" + cat >"$repo/openapi/openapi.json" <<'JSON' +{"openapi":"3.0.0","info":{"title":"Security Dir JSON API","version":"1.0.0"},"paths":{}} +JSON + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && bash scripts/check-openapi-security.sh -f '$repo/openapi'" + + [ "$status" -eq 0 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"-v $repo/openapi/openapi.json:/app/openapi.json"* ]] + [[ "$docker_call" == *"stoplight/spectral:latest lint /app/openapi.json"* ]] +} + +@test "run-openapi-docker mounts parent directory for file input and respects name/port" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/spec" + cat >"$repo/spec/openapi.yaml" <<'YAML' +openapi: 3.0.0 +info: + title: Preview API + version: 1.0.0 +paths: {} +YAML + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && bash scripts/run-openapi-docker.sh -n preview -p 9999:80 -f '$repo/spec/openapi.yaml'" + + [ "$status" -eq 0 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"--name preview"* ]] + [[ "$docker_call" == *"-v $repo/spec:/usr/share/nginx/html"* ]] + [[ "$docker_call" == *"-p 9999:80 nginx"* ]] +} + +@test "run-openapi-docker resolves .yaml to .json and mounts parent directory" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/spec" + cat >"$repo/spec/openapi.json" <<'JSON' +{"openapi":"3.0.0","info":{"title":"Preview JSON API","version":"1.0.0"},"paths":{}} +JSON + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && bash scripts/run-openapi-docker.sh -f '$repo/spec/openapi.yaml'" + + [ "$status" -eq 0 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"-v $repo/spec:/usr/share/nginx/html"* ]] +} + +@test "run-openapi-docker accepts direct .yml input and mounts parent directory" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/spec" + cat >"$repo/spec/openapi.yml" <<'YAML' +openapi: 3.0.0 +info: + title: Preview YML API + version: 1.0.0 +paths: {} +YAML + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && bash scripts/run-openapi-docker.sh -f '$repo/spec/openapi.yml'" + + [ "$status" -eq 0 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"-v $repo/spec:/usr/share/nginx/html"* ]] +} + +@test "run-openapi-docker accepts direct .json input and mounts parent directory" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/spec" + cat >"$repo/spec/openapi.json" <<'JSON' +{"openapi":"3.0.0","info":{"title":"Preview Direct JSON API","version":"1.0.0"},"paths":{}} +JSON + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && bash scripts/run-openapi-docker.sh -f '$repo/spec/openapi.json'" + + [ "$status" -eq 0 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"-v $repo/spec:/usr/share/nginx/html"* ]] +} + +@test "check-openapi-validation with directory prefers openapi.yaml over openapi.yml" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/openapi" + cat >"$repo/openapi/openapi.yaml" <<'YAML' +openapi: 3.0.0 +info: {title: YAML, version: 1.0.0} +paths: {} +YAML + cat >"$repo/openapi/openapi.yml" <<'YAML' +openapi: 3.0.0 +info: {title: YML, version: 1.0.0} +paths: {} +YAML + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && bash scripts/check-openapi-validation.sh -f '$repo/openapi'" + + [ "$status" -eq 0 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"-v $repo/openapi/openapi.yaml:/openapi.yaml"* ]] +} + +@test "check-openapi-validation with directory uses openapi.json when yaml/yml are absent" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/openapi" + cat >"$repo/openapi/openapi.json" <<'JSON' +{"openapi":"3.0.0","info":{"title":"JSON Dir API","version":"1.0.0"},"paths":{}} +JSON + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && bash scripts/check-openapi-validation.sh -f '$repo/openapi'" + + [ "$status" -eq 0 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"-v $repo/openapi/openapi.json:/openapi.json"* ]] +} + +@test "check-openapi-validation exits successfully when directory has no openapi spec" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/openapi-empty" + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && bash scripts/check-openapi-validation.sh -f '$repo/openapi-empty'" + + [ "$status" -eq 0 ] + [[ "$output" == *"skipping validation"* ]] + [ ! -f "$DOCKER_LOG" ] +} + +@test "check-openapi-security returns non-zero when docker scan fails" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/openapi" + cat >"$repo/openapi/openapi.yaml" <<'YAML' +openapi: 3.0.0 +info: + title: Test API + version: 1.0.0 +paths: {} +YAML + + export DOCKER_LOG="$repo/docker.log" + export DOCKER_EXIT_CODE=1 + run bash -c "cd '$repo' && bash scripts/check-openapi-security.sh -f '$repo/openapi/openapi.yaml'" + + [ "$status" -eq 1 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"stoplight/spectral:latest lint /app/openapi.yaml"* ]] +} + +@test "check-openapi-validation resolves default openapi path from repo root when run from subdirectory" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/openapi" "$repo/subdir" + cat >"$repo/openapi/openapi.yaml" <<'YAML' +openapi: 3.0.0 +info: + title: Root API + version: 1.0.0 +paths: {} +YAML + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo/subdir' && bash ../scripts/check-openapi-validation.sh" + + [ "$status" -eq 0 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"/openapi/openapi.yaml:/openapi.yaml"* ]] +} + +@test "check-openapi-validation works when script is piped to bash" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/openapi" + cat >"$repo/openapi/openapi.yaml" <<'YAML' +openapi: 3.0.0 +info: + title: Pipe API + version: 1.0.0 +paths: {} +YAML + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && cat scripts/check-openapi-validation.sh | bash" + + [ "$status" -eq 0 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"/openapi/openapi.yaml:/openapi.yaml"* ]] +} + +@test "check-openapi-validation supports piped execution with -f path relative to git root" { + repo="$(make_temp_repo)" + copy_openapi_scripts_into_repo "$repo" + install_docker_stub "$repo" + + mkdir -p "$repo/mail-examples/mail-example-openapi/openapi" + cat >"$repo/mail-examples/mail-example-openapi/openapi/openapi.yaml" <<'YAML' +openapi: 3.0.0 +info: + title: Nested API + version: 1.0.0 +paths: {} +YAML + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo' && cat scripts/check-openapi-validation.sh | bash -s -- -f mail-examples/mail-example-openapi/openapi/openapi.yaml" + + [ "$status" -eq 0 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"/mail-examples/mail-example-openapi/openapi/openapi.yaml:/openapi.yaml"* ]] +} + +@test "check-openapi-validation default path works for copied script in nested project scripts folder" { + repo="$(make_temp_repo)" + install_docker_stub "$repo" + + mkdir -p "$repo/mail-examples/mail-example-openapi/scripts" + mkdir -p "$repo/mail-examples/mail-example-openapi/openapi" + cp "$PROJECT_ROOT/scripts/check-openapi-validation.sh" \ + "$repo/mail-examples/mail-example-openapi/scripts/check-openapi-validation.sh" + chmod +x "$repo/mail-examples/mail-example-openapi/scripts/check-openapi-validation.sh" + + cat >"$repo/mail-examples/mail-example-openapi/openapi/openapi.yaml" <<'YAML' +openapi: 3.0.0 +info: + title: Nested Local API + version: 1.0.0 +paths: {} +YAML + + export DOCKER_LOG="$repo/docker.log" + run bash -c "cd '$repo/mail-examples/mail-example-openapi' && bash ./scripts/check-openapi-validation.sh" + + [ "$status" -eq 0 ] + docker_call="$(cat "$DOCKER_LOG")" + [[ "$docker_call" == *"/mail-example-openapi/openapi/openapi.yaml:/openapi.yaml"* ]] +} diff --git a/tests/fixtures/legacy_double_dot.swift b/tests/fixtures/legacy_double_dot.swift new file mode 100644 index 0000000..1261a16 --- /dev/null +++ b/tests/fixtures/legacy_double_dot.swift @@ -0,0 +1,11 @@ +// +// Address.swift +// HeaderProject +// +// Created by Test User on 2026. 02. 12.. + +import Foundation + +struct Address { + let city: String +} diff --git a/tests/fixtures/legacy_double_dot_fixed.swift b/tests/fixtures/legacy_double_dot_fixed.swift new file mode 100644 index 0000000..2e79148 --- /dev/null +++ b/tests/fixtures/legacy_double_dot_fixed.swift @@ -0,0 +1,11 @@ +// +// Address.swift +// HeaderProject +// +// Created by Test User on 2026. 02. 12. + +import Foundation + +struct Address { + let city: String +} diff --git a/tests/fixtures/missing_header.swift b/tests/fixtures/missing_header.swift new file mode 100644 index 0000000..6413cbf --- /dev/null +++ b/tests/fixtures/missing_header.swift @@ -0,0 +1,5 @@ +import Foundation + +struct Foo { + let value: Int +} diff --git a/tests/fixtures/missing_header_fixed.swift b/tests/fixtures/missing_header_fixed.swift new file mode 100644 index 0000000..f1aa9bd --- /dev/null +++ b/tests/fixtures/missing_header_fixed.swift @@ -0,0 +1,11 @@ +// +// Foo.swift +// HeaderProject +// +// Created by Binary Birds on 2026. 02. 12. + +import Foundation + +struct Foo { + let value: Int +} diff --git a/tests/fixtures/valid_single_dot.swift b/tests/fixtures/valid_single_dot.swift new file mode 100644 index 0000000..2e79148 --- /dev/null +++ b/tests/fixtures/valid_single_dot.swift @@ -0,0 +1,11 @@ +// +// Address.swift +// HeaderProject +// +// Created by Test User on 2026. 02. 12. + +import Foundation + +struct Address { + let city: String +} diff --git a/tests/fixtures/wrong_project.swift b/tests/fixtures/wrong_project.swift new file mode 100644 index 0000000..64b5b5f --- /dev/null +++ b/tests/fixtures/wrong_project.swift @@ -0,0 +1,11 @@ +// +// WrongProject.swift +// WrongProjectName +// +// Created by Test User on 2026. 02. 12. + +import Foundation + +struct WrongProject { + let value: String +} diff --git a/tests/fixtures/wrong_project_fixed.swift b/tests/fixtures/wrong_project_fixed.swift new file mode 100644 index 0000000..955dbd6 --- /dev/null +++ b/tests/fixtures/wrong_project_fixed.swift @@ -0,0 +1,11 @@ +// +// WrongProject.swift +// HeaderProject +// +// Created by Test User on 2026. 02. 12. + +import Foundation + +struct WrongProject { + let value: String +}