From 4aeda58a3774e4ca8d74b7873516f3d3108997be Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 18:37:47 +0000 Subject: [PATCH] test: add unit tests for sync-docs-from-node workflow scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add bats-core unit tests for the shell scripts used by the documentation sync workflow to prevent regressions when updating the docs. Tests cover: - version-utils.sh: validate_version function with various SemVer formats - sanitize-config.sh: config sanitization (RPC URL replacement, dev removal) - sanitize-docker-compose.sh: docker-compose sanitization (alloy removal) - sync.sh: file sync logic (add/update/delete, pattern filtering, exclusions) - aggregate-reports.sh: report aggregation and metric calculation Also adds a new GitHub workflow (test-workflow-scripts.yml) that runs these tests on PRs that modify the workflow scripts or actions. Co-Authored-By: Agustín Díaz --- .github/tests/aggregate-reports.bats | 192 +++++++++++++ .github/tests/sanitize-config.bats | 106 +++++++ .github/tests/sanitize-docker-compose.bats | 117 ++++++++ .github/tests/sync.bats | 300 ++++++++++++++++++++ .github/tests/version-utils.bats | 109 +++++++ .github/workflows/test-workflow-scripts.yml | 58 ++++ 6 files changed, 882 insertions(+) create mode 100644 .github/tests/aggregate-reports.bats create mode 100644 .github/tests/sanitize-config.bats create mode 100644 .github/tests/sanitize-docker-compose.bats create mode 100644 .github/tests/sync.bats create mode 100644 .github/tests/version-utils.bats create mode 100644 .github/workflows/test-workflow-scripts.yml diff --git a/.github/tests/aggregate-reports.bats b/.github/tests/aggregate-reports.bats new file mode 100644 index 00000000..6c981cb8 --- /dev/null +++ b/.github/tests/aggregate-reports.bats @@ -0,0 +1,192 @@ +#!/usr/bin/env bats + +# Unit tests for aggregate-reports.sh + +setup() { + export TEST_DIR=$(mktemp -d) + export SCRIPT_PATH="$BATS_TEST_DIRNAME/../scripts/aggregate-reports.sh" + export GITHUB_OUTPUT=$(mktemp) + + # Create sync-reports directory + mkdir -p "$TEST_DIR/sync-reports" + + # Change to test directory + cd "$TEST_DIR" +} + +teardown() { + rm -rf "$TEST_DIR" + rm -f "$GITHUB_OUTPUT" +} + +# ============================================ +# Tests for aggregate_sync_reports() +# ============================================ + +@test "aggregate-reports: calculates totals from single report" { + cat > "$TEST_DIR/sync-reports/sync_report_changelog.md" < "$TEST_DIR/sync-reports/sync_report_changelog.md" < "$TEST_DIR/sync-reports/sync_report_api_gen.md" < "$TEST_DIR/sync-reports/sync_report_config.md" < "$TEST_DIR/sync-reports/sync_report_config.md" < "$TEST_DIR/sync-reports/sync_report_test.md" < "$TEST_DIR/sync-reports/sync_report_${sync_type}.md" < "$TEST_DIR/config.yaml" < "$TEST_DIR/config.yaml" < "$TEST_DIR/config.yaml" < "$TEST_DIR/config.yaml" < "$TEST_DIR/docker-compose.yaml" < "$TEST_DIR/docker-compose.yaml" < "$TEST_DIR/docker-compose.yaml" < "$TEST_DIR/docker-compose.yaml" < "$TEST_DIR/source/file.yaml" + + run bash "$SCRIPT_PATH" "test" "Test" "$TEST_DIR/source/file.yaml" "$TEST_DIR/target/file.yaml" ".*" "" + [ "$status" -eq 0 ] + + # Check file was copied + [ -f "$TEST_DIR/target/file.yaml" ] + + # Check metrics + run grep "added=1" "$GITHUB_OUTPUT" + [ "$status" -eq 0 ] +} + +@test "sync: updates existing single file when content differs" { + echo "old content" > "$TEST_DIR/target/file.yaml" + echo "new content" > "$TEST_DIR/source/file.yaml" + + run bash "$SCRIPT_PATH" "test" "Test" "$TEST_DIR/source/file.yaml" "$TEST_DIR/target/file.yaml" ".*" "" + [ "$status" -eq 0 ] + + # Check file was updated + run cat "$TEST_DIR/target/file.yaml" + [ "$output" = "new content" ] + + # Check metrics + run grep "updated=1" "$GITHUB_OUTPUT" + [ "$status" -eq 0 ] +} + +@test "sync: does not update file when content is same" { + echo "same content" > "$TEST_DIR/source/file.yaml" + echo "same content" > "$TEST_DIR/target/file.yaml" + + run bash "$SCRIPT_PATH" "test" "Test" "$TEST_DIR/source/file.yaml" "$TEST_DIR/target/file.yaml" ".*" "" + [ "$status" -eq 0 ] + + # Check metrics show no changes + run grep "updated=0" "$GITHUB_OUTPUT" + [ "$status" -eq 0 ] + run grep "added=0" "$GITHUB_OUTPUT" + [ "$status" -eq 0 ] +} + +# ============================================ +# Tests for directory sync +# ============================================ + +@test "sync: adds new files from directory" { + echo "content1" > "$TEST_DIR/source/file1.mdx" + echo "content2" > "$TEST_DIR/source/file2.mdx" + + run bash "$SCRIPT_PATH" "test" "Test" "$TEST_DIR/source" "$TEST_DIR/target" ".*" "" + [ "$status" -eq 0 ] + + # Check files were copied + [ -f "$TEST_DIR/target/file1.mdx" ] + [ -f "$TEST_DIR/target/file2.mdx" ] + + # Check metrics + run grep "added=2" "$GITHUB_OUTPUT" + [ "$status" -eq 0 ] +} + +@test "sync: updates changed files in directory" { + echo "old content" > "$TEST_DIR/target/file1.mdx" + echo "new content" > "$TEST_DIR/source/file1.mdx" + + run bash "$SCRIPT_PATH" "test" "Test" "$TEST_DIR/source" "$TEST_DIR/target" ".*" "" + [ "$status" -eq 0 ] + + # Check file was updated + run cat "$TEST_DIR/target/file1.mdx" + [ "$output" = "new content" ] + + # Check metrics + run grep "updated=1" "$GITHUB_OUTPUT" + [ "$status" -eq 0 ] +} + +@test "sync: deletes orphaned files from target" { + echo "content" > "$TEST_DIR/source/file1.mdx" + echo "orphan" > "$TEST_DIR/target/orphan.mdx" + + run bash "$SCRIPT_PATH" "test" "Test" "$TEST_DIR/source" "$TEST_DIR/target" ".*" "" + [ "$status" -eq 0 ] + + # Check orphan was deleted + [ ! -f "$TEST_DIR/target/orphan.mdx" ] + + # Check metrics + run grep "deleted=1" "$GITHUB_OUTPUT" + [ "$status" -eq 0 ] +} + +@test "sync: preserves _meta.json file" { + echo "content" > "$TEST_DIR/source/file1.mdx" + echo '{"file1": "File 1"}' > "$TEST_DIR/target/_meta.json" + + run bash "$SCRIPT_PATH" "test" "Test" "$TEST_DIR/source" "$TEST_DIR/target" ".*" "" + [ "$status" -eq 0 ] + + # Check _meta.json was preserved + [ -f "$TEST_DIR/target/_meta.json" ] +} + +# ============================================ +# Tests for pattern filtering +# ============================================ + +@test "sync: filters files by pattern - includes matching" { + echo "gen content" > "$TEST_DIR/source/gen_method.mdx" + echo "other content" > "$TEST_DIR/source/other_method.mdx" + + run bash "$SCRIPT_PATH" "test" "Test" "$TEST_DIR/source" "$TEST_DIR/target" "gen_.*" "" + [ "$status" -eq 0 ] + + # Check only matching file was copied + [ -f "$TEST_DIR/target/gen_method.mdx" ] + [ ! -f "$TEST_DIR/target/other_method.mdx" ] +} + +@test "sync: filters files by pattern - excludes non-matching" { + echo "content1" > "$TEST_DIR/source/gen_call.mdx" + echo "content2" > "$TEST_DIR/source/gen_send.mdx" + echo "content3" > "$TEST_DIR/source/eth_call.mdx" + + run bash "$SCRIPT_PATH" "test" "Test" "$TEST_DIR/source" "$TEST_DIR/target" "gen_.*" "" + [ "$status" -eq 0 ] + + # Check only gen_ files were copied + [ -f "$TEST_DIR/target/gen_call.mdx" ] + [ -f "$TEST_DIR/target/gen_send.mdx" ] + [ ! -f "$TEST_DIR/target/eth_call.mdx" ] +} + +@test "sync: handles .* pattern (match all)" { + echo "content1" > "$TEST_DIR/source/file1.mdx" + echo "content2" > "$TEST_DIR/source/file2.mdx" + + run bash "$SCRIPT_PATH" "test" "Test" "$TEST_DIR/source" "$TEST_DIR/target" ".*" "" + [ "$status" -eq 0 ] + + # Check all files were copied + [ -f "$TEST_DIR/target/file1.mdx" ] + [ -f "$TEST_DIR/target/file2.mdx" ] +} + +# ============================================ +# Tests for file exclusions +# ============================================ + +@test "sync: excludes README files" { + echo "content" > "$TEST_DIR/source/file1.mdx" + echo "readme" > "$TEST_DIR/source/README.mdx" + + run bash "$SCRIPT_PATH" "test" "Test" "$TEST_DIR/source" "$TEST_DIR/target" ".*" "README" + [ "$status" -eq 0 ] + + # Check README was excluded + [ -f "$TEST_DIR/target/file1.mdx" ] + [ ! -f "$TEST_DIR/target/README.mdx" ] +} + +@test "sync: excludes multiple files" { + echo "content" > "$TEST_DIR/source/file1.mdx" + echo "readme" > "$TEST_DIR/source/README.mdx" + echo "changelog" > "$TEST_DIR/source/CHANGELOG.mdx" + + run bash "$SCRIPT_PATH" "test" "Test" "$TEST_DIR/source" "$TEST_DIR/target" ".*" "README,CHANGELOG" + [ "$status" -eq 0 ] + + # Check excluded files were not copied + [ -f "$TEST_DIR/target/file1.mdx" ] + [ ! -f "$TEST_DIR/target/README.mdx" ] + [ ! -f "$TEST_DIR/target/CHANGELOG.mdx" ] +} + +@test "sync: default exclusions work" { + echo "content" > "$TEST_DIR/source/file1.mdx" + echo "readme" > "$TEST_DIR/source/README.mdx" + echo "changelog" > "$TEST_DIR/source/CHANGELOG.mdx" + echo "gitignore" > "$TEST_DIR/source/.gitignore.mdx" + + # Use default exclusions + run bash "$SCRIPT_PATH" "test" "Test" "$TEST_DIR/source" "$TEST_DIR/target" ".*" "README,CHANGELOG,.gitignore,.gitkeep" + [ "$status" -eq 0 ] + + # Check only file1 was copied + [ -f "$TEST_DIR/target/file1.mdx" ] + [ ! -f "$TEST_DIR/target/README.mdx" ] + [ ! -f "$TEST_DIR/target/CHANGELOG.mdx" ] +} + +# ============================================ +# Tests for .md to .mdx conversion +# ============================================ + +@test "sync: converts .md files to .mdx" { + echo "markdown content" > "$TEST_DIR/source/file.md" + + run bash "$SCRIPT_PATH" "test" "Test" "$TEST_DIR/source" "$TEST_DIR/target" ".*" "" + [ "$status" -eq 0 ] + + # Check file was converted to .mdx + [ -f "$TEST_DIR/target/file.mdx" ] + [ ! -f "$TEST_DIR/target/file.md" ] +} + +@test "sync: handles mixed .md and .mdx files" { + echo "md content" > "$TEST_DIR/source/file1.md" + echo "mdx content" > "$TEST_DIR/source/file2.mdx" + + run bash "$SCRIPT_PATH" "test" "Test" "$TEST_DIR/source" "$TEST_DIR/target" ".*" "" + [ "$status" -eq 0 ] + + # Check both files exist as .mdx + [ -f "$TEST_DIR/target/file1.mdx" ] + [ -f "$TEST_DIR/target/file2.mdx" ] +} + +# ============================================ +# Tests for report generation +# ============================================ + +@test "sync: generates sync report" { + echo "content" > "$TEST_DIR/source/file1.mdx" + + cd "$TEST_DIR" + run bash "$SCRIPT_PATH" "test_type" "Test Title" "$TEST_DIR/source" "$TEST_DIR/target" ".*" "" + [ "$status" -eq 0 ] + + # Check report was created + [ -f "artifacts/sync_report_test_type.md" ] + + # Check report contains expected content + run grep "Test Title" "artifacts/sync_report_test_type.md" + [ "$status" -eq 0 ] + + run grep "Added" "artifacts/sync_report_test_type.md" + [ "$status" -eq 0 ] +} + +# ============================================ +# Tests for error handling +# ============================================ + +@test "sync: fails when source does not exist" { + run bash "$SCRIPT_PATH" "test" "Test" "$TEST_DIR/nonexistent" "$TEST_DIR/target" ".*" "" + [ "$status" -eq 1 ] + [[ "$output" == *"Source not found"* ]] +} + +@test "sync: handles empty source directory" { + # Source directory exists but is empty + run bash "$SCRIPT_PATH" "test" "Test" "$TEST_DIR/source" "$TEST_DIR/target" ".*" "" + [ "$status" -eq 0 ] + + # Check metrics show no changes + run grep "total=0" "$GITHUB_OUTPUT" + [ "$status" -eq 0 ] +} + +@test "sync: does not delete when source has no matching files" { + # Create target file + echo "existing" > "$TEST_DIR/target/existing.mdx" + + # Source has no .md or .mdx files + echo "not markdown" > "$TEST_DIR/source/file.txt" + + run bash "$SCRIPT_PATH" "test" "Test" "$TEST_DIR/source" "$TEST_DIR/target" ".*" "" + [ "$status" -eq 0 ] + + # Check existing file was NOT deleted (safety feature) + [ -f "$TEST_DIR/target/existing.mdx" ] +} diff --git a/.github/tests/version-utils.bats b/.github/tests/version-utils.bats new file mode 100644 index 00000000..d8786e96 --- /dev/null +++ b/.github/tests/version-utils.bats @@ -0,0 +1,109 @@ +#!/usr/bin/env bats + +# Unit tests for version-utils.sh + +setup() { + # Load the script functions + source "$BATS_TEST_DIRNAME/../scripts/version-utils.sh" 2>/dev/null || true + + # Create temp directory for test outputs + export GITHUB_OUTPUT=$(mktemp) +} + +teardown() { + rm -f "$GITHUB_OUTPUT" +} + +# ============================================ +# Tests for validate_version() +# ============================================ + +@test "validate_version: accepts 'latest'" { + run validate_version "latest" + [ "$status" -eq 0 ] +} + +@test "validate_version: accepts simple semver v1.0.0" { + run validate_version "v1.0.0" + [ "$status" -eq 0 ] +} + +@test "validate_version: accepts v0.4.3" { + run validate_version "v0.4.3" + [ "$status" -eq 0 ] +} + +@test "validate_version: accepts v10.20.30" { + run validate_version "v10.20.30" + [ "$status" -eq 0 ] +} + +@test "validate_version: accepts prerelease v1.2.3-rc.1" { + run validate_version "v1.2.3-rc.1" + [ "$status" -eq 0 ] +} + +@test "validate_version: accepts prerelease v1.2.3-alpha.2" { + run validate_version "v1.2.3-alpha.2" + [ "$status" -eq 0 ] +} + +@test "validate_version: accepts prerelease v1.2.3-beta.1" { + run validate_version "v1.2.3-beta.1" + [ "$status" -eq 0 ] +} + +@test "validate_version: accepts build metadata v1.2.3+build.7" { + run validate_version "v1.2.3+build.7" + [ "$status" -eq 0 ] +} + +@test "validate_version: accepts full semver v1.2.3-beta.1+exp.sha.5114f85" { + run validate_version "v1.2.3-beta.1+exp.sha.5114f85" + [ "$status" -eq 0 ] +} + +@test "validate_version: accepts testnet version v0.4.0-testnet123" { + run validate_version "v0.4.0-testnet123" + [ "$status" -eq 0 ] +} + +@test "validate_version: rejects version without v prefix" { + run validate_version "1.0.0" + [ "$status" -eq 1 ] +} + +@test "validate_version: rejects invalid format 'main'" { + run validate_version "main" + [ "$status" -eq 1 ] +} + +@test "validate_version: rejects invalid format 'release-1.0'" { + run validate_version "release-1.0" + [ "$status" -eq 1 ] +} + +@test "validate_version: rejects empty string" { + run validate_version "" + [ "$status" -eq 1 ] +} + +@test "validate_version: rejects version with only two parts v1.0" { + run validate_version "v1.0" + [ "$status" -eq 1 ] +} + +@test "validate_version: rejects version with four parts v1.0.0.0" { + run validate_version "v1.0.0.0" + [ "$status" -eq 1 ] +} + +@test "validate_version: rejects version with spaces" { + run validate_version "v1.0.0 " + [ "$status" -eq 1 ] +} + +@test "validate_version: rejects version with leading space" { + run validate_version " v1.0.0" + [ "$status" -eq 1 ] +} diff --git a/.github/workflows/test-workflow-scripts.yml b/.github/workflows/test-workflow-scripts.yml new file mode 100644 index 00000000..d0e6c091 --- /dev/null +++ b/.github/workflows/test-workflow-scripts.yml @@ -0,0 +1,58 @@ +name: Test Workflow Scripts + +on: + pull_request: + paths: + - '.github/scripts/**' + - '.github/actions/**' + - '.github/tests/**' + - '.github/workflows/sync-docs-from-node.yml' + - '.github/workflows/test-workflow-scripts.yml' + push: + branches: + - main + paths: + - '.github/scripts/**' + - '.github/actions/**' + - '.github/tests/**' + - '.github/workflows/sync-docs-from-node.yml' + - '.github/workflows/test-workflow-scripts.yml' + workflow_dispatch: + +jobs: + test-scripts: + name: Run Shell Script Tests + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install bats-core + run: | + sudo apt-get update + sudo apt-get install -y bats + + - name: Install yq (required for sanitization scripts) + run: | + sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 + sudo chmod +x /usr/local/bin/yq + + - name: Run version-utils tests + run: bats .github/tests/version-utils.bats + + - name: Run sanitize-config tests + run: bats .github/tests/sanitize-config.bats + + - name: Run sanitize-docker-compose tests + run: bats .github/tests/sanitize-docker-compose.bats + + - name: Run sync tests + run: bats .github/tests/sync.bats + + - name: Run aggregate-reports tests + run: bats .github/tests/aggregate-reports.bats + + - name: Run all tests (summary) + run: | + echo "Running all bats tests..." + bats .github/tests/*.bats --tap