Add comprehensive testing infrastructure

This commit adds a complete testing setup for the prettier_action:

- Adds BATS (Bash Automated Testing System) testing framework
- Creates unit tests for _git_setup() and _git_changed() functions
- Creates plugin validation tests to ensure proper prettier plugin format
- Creates integration tests for end-to-end workflows
- Adds automated test runner script (tests/run_tests.sh)
- Adds GitHub Actions workflow for CI/CD testing
- Includes ShellCheck linting for bash scripts
- Updates README with comprehensive testing documentation
- Updates .gitignore to exclude test artifacts

Test coverage includes:
- Git configuration with different identity modes
- File change detection
- Plugin name validation (official, community, and scoped formats)
- Working directory handling
- node_modules cleanup
- package-lock.json restoration
- only_changed file filtering
- Dry run behavior

The test suite can be run locally with ./tests/run_tests.sh and runs
automatically on all pushes and pull requests.
This commit is contained in:
Claude
2025-11-17 16:11:51 +00:00
parent 8c18391fdc
commit 2b1305afee
8 changed files with 762 additions and 0 deletions

81
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,81 @@
name: Run Tests
on:
push:
branches: [ master, dev, 'claude/**' ]
pull_request:
branches: [ master, dev ]
workflow_dispatch:
jobs:
test:
name: Run BATS Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Git
run: |
git config --global user.name "GitHub Actions"
git config --global user.email "actions@github.com"
- name: Install BATS
run: |
cd tests
./run_tests.sh --install-only
- name: Run unit tests
run: |
cd tests
./bats/bin/bats unit_tests.bats
- name: Run plugin validation tests
run: |
cd tests
./bats/bin/bats plugin_validation_tests.bats
- name: Run integration tests
run: |
cd tests
./bats/bin/bats integration_tests.bats
- name: Run all tests with runner script
run: |
./tests/run_tests.sh
test-action:
name: Test Action End-to-End
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Create test files
run: |
echo "const x=1;const y=2;" > test.js
echo "function foo(){return 'bar';}" > test2.js
- name: Run prettier action in dry mode
uses: ./
with:
dry: true
prettier_options: "--write --check test*.js"
no_commit: true
github_token: ${{ secrets.GITHUB_TOKEN }}
shellcheck:
name: Shellcheck
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@master
with:
scandir: '.'
severity: warning

3
.gitignore vendored
View File

@@ -2,3 +2,6 @@
.DS_Store
# NPM
node_modules/
# Testing
tests/bats/
tests/test_temp_*/

View File

@@ -158,6 +158,73 @@ jobs:
More documentation for writing a workflow can be found [here](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions).
## Testing
This project includes comprehensive test coverage using [BATS (Bash Automated Testing System)](https://github.com/bats-core/bats-core).
### Running Tests Locally
To run the tests locally, execute the test runner script:
```bash
./tests/run_tests.sh
```
This script will automatically:
1. Install BATS and required dependencies if not already present
2. Run all unit tests
3. Run plugin validation tests
4. Run integration tests
### Test Structure
The test suite is organized into three main categories:
- **`tests/unit_tests.bats`** - Unit tests for bash functions in `entrypoint.sh`
- Tests for `_git_setup()` function with different identity configurations
- Tests for `_git_changed()` function for detecting file changes
- **`tests/plugin_validation_tests.bats`** - Tests for Prettier plugin validation logic
- Validates official `@prettier/plugin-*` format
- Validates community `prettier-plugin-*` format
- Validates scoped `@scope/prettier-plugin-*` format
- Ensures invalid plugin names are rejected
- **`tests/integration_tests.bats`** - Integration tests for end-to-end workflows
- Tests working directory handling
- Tests node_modules cleanup
- Tests package-lock.json restoration
- Tests file filtering for `only_changed` mode
- Tests dry run behavior
### Manual BATS Installation
If you prefer to install BATS manually:
```bash
cd tests
./run_tests.sh --install-only
```
Then run individual test files:
```bash
./tests/bats/bin/bats tests/unit_tests.bats
./tests/bats/bin/bats tests/plugin_validation_tests.bats
./tests/bats/bin/bats tests/integration_tests.bats
```
### Continuous Integration
Tests are automatically run on every push and pull request via GitHub Actions. See [`.github/workflows/test.yml`](.github/workflows/test.yml) for the CI configuration.
The CI workflow includes:
- Unit tests
- Plugin validation tests
- Integration tests
- End-to-end action testing in dry mode
- ShellCheck linting for bash scripts
## Issues
Please report all bugs and feature request using the [GitHub issues function](https://github.com/creyD/prettier_action/issues/new). Thanks!

View File

@@ -0,0 +1,195 @@
#!/usr/bin/env bats
# Integration tests for prettier_action
# These tests verify the overall behavior of the action
load 'test_helper'
setup() {
setup_test_repo
mock_github_env
set_default_inputs
export SCRIPT_DIR="$(cd "$(dirname "${BATS_TEST_DIRNAME}")" && pwd)"
}
teardown() {
teardown_test_repo
}
@test "Action sets correct working directory when not specified" {
export INPUT_WORKING_DIRECTORY=""
export GITHUB_ACTION_PATH="/test/path"
# We'll test that the directory change logic works correctly
# by verifying the default assignment
result=$(bash -c '
INPUT_WORKING_DIRECTORY=""
GITHUB_ACTION_PATH="/test/path"
if [ -z "$INPUT_WORKING_DIRECTORY" ]; then
INPUT_WORKING_DIRECTORY=$GITHUB_ACTION_PATH
fi
echo "$INPUT_WORKING_DIRECTORY"
')
[ "$result" = "/test/path" ]
}
@test "Action preserves working directory when specified" {
export INPUT_WORKING_DIRECTORY="/custom/path"
result=$(bash -c '
INPUT_WORKING_DIRECTORY="/custom/path"
GITHUB_ACTION_PATH="/test/path"
if [ -z "$INPUT_WORKING_DIRECTORY" ]; then
INPUT_WORKING_DIRECTORY=$GITHUB_ACTION_PATH
fi
echo "$INPUT_WORKING_DIRECTORY"
')
[ "$result" = "/custom/path" ]
}
@test "Clean node folder removes node_modules when it exists" {
# Create node_modules directory
mkdir -p node_modules
echo "test" > node_modules/test.txt
# Simulate the clean logic
INPUT_CLEAN_NODE_FOLDER=true
if $INPUT_CLEAN_NODE_FOLDER; then
if [ -d 'node_modules' ]; then
rm -r node_modules/
fi
fi
# Verify node_modules was removed
[ ! -d "node_modules" ]
}
@test "Clean node folder handles missing node_modules gracefully" {
# Ensure no node_modules exists
[ ! -d "node_modules" ]
# Simulate the clean logic
INPUT_CLEAN_NODE_FOLDER=true
run bash -c '
if $INPUT_CLEAN_NODE_FOLDER; then
if [ -d "node_modules" ]; then
rm -r node_modules/
echo "Deleted"
else
echo "No node_modules/ folder."
fi
fi
'
[ "$status" -eq 0 ]
[[ "$output" =~ "No node_modules/ folder." ]]
}
@test "Package-lock.json is restored when it exists" {
# Create a package-lock.json and commit it
echo '{"name": "test"}' > package-lock.json
git add package-lock.json
git commit -m "Add package-lock.json"
# Modify it
echo '{"name": "modified"}' > package-lock.json
# Restore it using git checkout
git checkout -- package-lock.json
# Verify it was restored
content=$(cat package-lock.json)
[[ "$content" =~ '"name": "test"' ]]
}
@test "Package-lock.json restore handles missing file gracefully" {
# Ensure no package-lock.json exists
[ ! -f "package-lock.json" ]
# Try to restore (should not fail)
run bash -c '
if [ -f "package-lock.json" ]; then
git checkout -- package-lock.json || echo "No package-lock.json file tracked by git."
else
echo "No package-lock.json file."
fi
'
[ "$status" -eq 0 ]
[[ "$output" =~ "No package-lock.json file." ]]
}
@test "File pattern logic for only_changed mode filters correctly" {
# Create initial commit
echo "file1" > file1.txt
echo "file2" > file2.txt
git add .
git commit -m "Initial commit"
# Modify only file1
echo "modified" > file1.txt
git add file1.txt
git commit -m "Modify file1"
# Modify both files
echo "changed1" > file1.txt
echo "changed2" > file2.txt
# Get files changed in previous commit
git diff --name-only HEAD HEAD~1 > /tmp/prev.txt
# Get files with current changes
git diff --name-only HEAD > /tmp/cur.txt
# Verify file1.txt is in prev.txt (it was changed in last commit)
run grep "file1.txt" /tmp/prev.txt
[ "$status" -eq 0 ]
# Verify both files are in cur.txt (both have current changes)
run grep "file1.txt" /tmp/cur.txt
[ "$status" -eq 0 ]
run grep "file2.txt" /tmp/cur.txt
[ "$status" -eq 0 ]
# Files in cur.txt but not in prev.txt should be reset
# In this case, file2.txt should be reset
for file in $(comm -1 -3 /tmp/prev.txt /tmp/cur.txt); do
[ "$file" = "file2.txt" ]
done
}
@test "Dry run mode detects unpretty files" {
# This tests the logic flow for dry run
# We simulate detecting changes
# Create a test file
echo "test" > test.txt
# Simulate git detecting changes
run bash -c '
source tests/test_helper.bash
setup_test_repo
echo "test" > test.txt
_git_changed
'
[ "$status" -eq 0 ]
}
@test "No changes scenario is handled correctly" {
# Create and commit a file
echo "test" > test.txt
git add test.txt
git commit -m "Add test file"
# Verify no changes
run bash -c '
source tests/test_helper.bash
load_script_functions entrypoint.sh
_git_changed
'
[ "$status" -eq 1 ]
}

View File

@@ -0,0 +1,99 @@
#!/usr/bin/env bats
# Integration tests for prettier plugin validation
load 'test_helper'
setup() {
setup_test_repo
mock_github_env
set_default_inputs
}
teardown() {
teardown_test_repo
}
# Test valid prettier plugin patterns
@test "Valid @prettier/plugin-* format should pass validation" {
# Test the regex pattern used in entrypoint.sh
plugin="@prettier/plugin-php"
run bash -c "echo '$plugin' | grep -Eq '(@prettier\/plugin-|(@[a-z\-]+\/)?prettier-plugin-){1}([a-z\-]+)'"
[ "$status" -eq 0 ]
}
@test "Valid prettier-plugin-* format should pass validation" {
plugin="prettier-plugin-java"
run bash -c "echo '$plugin' | grep -Eq '(@prettier\/plugin-|(@[a-z\-]+\/)?prettier-plugin-){1}([a-z\-]+)'"
[ "$status" -eq 0 ]
}
@test "Valid @scope/prettier-plugin-* format should pass validation" {
plugin="@company/prettier-plugin-custom"
run bash -c "echo '$plugin' | grep -Eq '(@prettier\/plugin-|(@[a-z\-]+\/)?prettier-plugin-){1}([a-z\-]+)'"
[ "$status" -eq 0 ]
}
@test "Invalid plugin name should fail validation" {
plugin="some-random-package"
run bash -c "echo '$plugin' | grep -Eq '(@prettier\/plugin-|(@[a-z\-]+\/)?prettier-plugin-){1}([a-z\-]+)'"
[ "$status" -eq 1 ]
}
@test "Invalid plugin with wrong prefix should fail validation" {
plugin="@other/plugin-something"
run bash -c "echo '$plugin' | grep -Eq '(@prettier\/plugin-|(@[a-z\-]+\/)?prettier-plugin-){1}([a-z\-]+)'"
[ "$status" -eq 1 ]
}
@test "Multiple valid plugins should all pass validation" {
plugins="@prettier/plugin-php prettier-plugin-java @scope/prettier-plugin-custom"
for plugin in $plugins; do
run bash -c "echo '$plugin' | grep -Eq '(@prettier\/plugin-|(@[a-z\-]+\/)?prettier-plugin-){1}([a-z\-]+)'"
[ "$status" -eq 0 ]
done
}
@test "Plugin name with uppercase should fail validation" {
plugin="@prettier/plugin-PHP"
run bash -c "echo '$plugin' | grep -Eq '(@prettier\/plugin-|(@[a-z\-]+\/)?prettier-plugin-){1}([a-z\-]+)'"
[ "$status" -eq 1 ]
}
@test "Plugin name with numbers or underscores should fail validation" {
plugin1="@prettier/plugin-test123"
plugin2="prettier-plugin-test_name"
run bash -c "echo '$plugin1' | grep -Eq '(@prettier\/plugin-|(@[a-z\-]+\/)?prettier-plugin-){1}([a-z\-]+)'"
[ "$status" -eq 1 ]
run bash -c "echo '$plugin2' | grep -Eq '(@prettier\/plugin-|(@[a-z\-]+\/)?prettier-plugin-){1}([a-z\-]+)'"
[ "$status" -eq 1 ]
}
@test "Official prettier plugins should be recognized" {
# List of known official prettier plugins
plugins=(
"@prettier/plugin-php"
"@prettier/plugin-ruby"
"@prettier/plugin-xml"
)
for plugin in "${plugins[@]}"; do
run bash -c "echo '$plugin' | grep -Eq '(@prettier\/plugin-|(@[a-z\-]+\/)?prettier-plugin-){1}([a-z\-]+)'"
[ "$status" -eq 0 ]
done
}

115
tests/run_tests.sh Executable file
View File

@@ -0,0 +1,115 @@
#!/usr/bin/env bash
# Test runner script for prettier_action
# This script installs BATS and runs all tests
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
BATS_VERSION="v1.11.0"
BATS_INSTALL_DIR="$SCRIPT_DIR/bats"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo "========================================="
echo "Prettier Action Test Runner"
echo "========================================="
echo ""
# Function to install BATS
install_bats() {
echo -e "${YELLOW}Installing BATS (Bash Automated Testing System)...${NC}"
if [ -d "$BATS_INSTALL_DIR" ]; then
echo "BATS already installed at $BATS_INSTALL_DIR"
return 0
fi
# Clone BATS
git clone --depth 1 --branch "$BATS_VERSION" https://github.com/bats-core/bats-core.git "$BATS_INSTALL_DIR"
# Clone support libraries
mkdir -p "$BATS_INSTALL_DIR/test_helper"
git clone --depth 1 https://github.com/bats-core/bats-support.git "$BATS_INSTALL_DIR/test_helper/bats-support"
git clone --depth 1 https://github.com/bats-core/bats-assert.git "$BATS_INSTALL_DIR/test_helper/bats-assert"
echo -e "${GREEN}BATS installed successfully!${NC}"
echo ""
}
# Function to check if BATS is available
check_bats() {
if [ -x "$BATS_INSTALL_DIR/bin/bats" ]; then
return 0
fi
return 1
}
# Main execution
main() {
cd "$PROJECT_ROOT"
# Check if BATS is installed, if not install it
if ! check_bats; then
install_bats
fi
echo -e "${YELLOW}Running tests...${NC}"
echo ""
# Run all test files
TEST_FILES=(
"$SCRIPT_DIR/unit_tests.bats"
"$SCRIPT_DIR/plugin_validation_tests.bats"
"$SCRIPT_DIR/integration_tests.bats"
)
FAILED=0
for test_file in "${TEST_FILES[@]}"; do
if [ -f "$test_file" ]; then
echo "Running $(basename "$test_file")..."
if "$BATS_INSTALL_DIR/bin/bats" "$test_file"; then
echo -e "${GREEN}$(basename "$test_file") passed${NC}"
else
echo -e "${RED}$(basename "$test_file") failed${NC}"
FAILED=1
fi
echo ""
fi
done
echo "========================================="
if [ $FAILED -eq 0 ]; then
echo -e "${GREEN}All tests passed!${NC}"
exit 0
else
echo -e "${RED}Some tests failed!${NC}"
exit 1
fi
}
# Parse command line arguments
case "${1:-}" in
--install-only)
install_bats
exit 0
;;
--help)
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " --install-only Only install BATS without running tests"
echo " --help Show this help message"
echo ""
exit 0
;;
*)
main
;;
esac

78
tests/test_helper.bash Normal file
View File

@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# Test helper functions for prettier_action tests
# Set up a temporary test directory
setup_test_repo() {
export TEST_TEMP_DIR="$(mktemp -d)"
cd "$TEST_TEMP_DIR" || exit 1
git init
git config user.name "Test User"
git config user.email "test@example.com"
}
# Clean up temporary test directory
teardown_test_repo() {
if [ -n "$TEST_TEMP_DIR" ] && [ -d "$TEST_TEMP_DIR" ]; then
rm -rf "$TEST_TEMP_DIR"
fi
}
# Create a sample file for testing
create_sample_file() {
local filename="${1:-test.js}"
local content="${2:-const x=1;const y=2;}"
echo "$content" > "$filename"
}
# Create a sample package.json
create_package_json() {
cat > package.json << 'EOF'
{
"name": "test-project",
"version": "1.0.0",
"description": "Test project"
}
EOF
}
# Mock git environment variables for GitHub Actions
mock_github_env() {
export GITHUB_ACTOR="${GITHUB_ACTOR:-test-actor}"
export GITHUB_ACTOR_ID="${GITHUB_ACTOR_ID:-12345}"
export GITHUB_ACTION_PATH="${GITHUB_ACTION_PATH:-/app}"
export GITHUB_BASE_REF="${GITHUB_BASE_REF:-main}"
export GITHUB_STEP_SUMMARY="${GITHUB_STEP_SUMMARY:-/dev/null}"
export INPUT_GITHUB_TOKEN="${INPUT_GITHUB_TOKEN:-test-token}"
}
# Set default input environment variables
set_default_inputs() {
export INPUT_WORKING_DIRECTORY="${INPUT_WORKING_DIRECTORY:-}"
export INPUT_PRETTIER_VERSION="${INPUT_PRETTIER_VERSION:-latest}"
export INPUT_PRETTIER_OPTIONS="${INPUT_PRETTIER_OPTIONS:---write **/*.js}"
export INPUT_PRETTIER_PLUGINS="${INPUT_PRETTIER_PLUGINS:-}"
export INPUT_ALLOW_OTHER_PLUGINS="${INPUT_ALLOW_OTHER_PLUGINS:-false}"
export INPUT_CLEAN_NODE_FOLDER="${INPUT_CLEAN_NODE_FOLDER:-true}"
export INPUT_ONLY_CHANGED="${INPUT_ONLY_CHANGED:-false}"
export INPUT_ONLY_CHANGED_PR="${INPUT_ONLY_CHANGED_PR:-false}"
export INPUT_FILE_PATTERN="${INPUT_FILE_PATTERN:-*}"
export INPUT_DRY="${INPUT_DRY:-false}"
export INPUT_NO_COMMIT="${INPUT_NO_COMMIT:-false}"
export INPUT_SAME_COMMIT="${INPUT_SAME_COMMIT:-false}"
export INPUT_COMMIT_MESSAGE="${INPUT_COMMIT_MESSAGE:-Automated formatting}"
export INPUT_COMMIT_DESCRIPTION="${INPUT_COMMIT_DESCRIPTION:-}"
export INPUT_COMMIT_OPTIONS="${INPUT_COMMIT_OPTIONS:-}"
export INPUT_PUSH_OPTIONS="${INPUT_PUSH_OPTIONS:-}"
export INPUT_GIT_IDENTITY="${INPUT_GIT_IDENTITY:-actions}"
}
# Load a bash script without executing it (for testing functions)
load_script_functions() {
local script_path="$1"
# Source only the function definitions, not the main program
# We extract functions by finding lines between function definitions and the main program block
sed -n '/^_git_setup/,/^}/p' "$script_path" > "$TEST_TEMP_DIR/functions.sh"
sed -n '/^_git_changed/,/^}/p' "$script_path" >> "$TEST_TEMP_DIR/functions.sh"
source "$TEST_TEMP_DIR/functions.sh"
}

124
tests/unit_tests.bats Normal file
View File

@@ -0,0 +1,124 @@
#!/usr/bin/env bats
# Unit tests for prettier_action entrypoint.sh functions
load 'test_helper'
setup() {
setup_test_repo
mock_github_env
set_default_inputs
# Load the functions from entrypoint.sh
export SCRIPT_DIR="$(cd "$(dirname "${BATS_TEST_DIRNAME}")" && pwd)"
load_script_functions "$SCRIPT_DIR/entrypoint.sh"
}
teardown() {
teardown_test_repo
}
# Test _git_setup function with 'actions' identity
@test "_git_setup creates .netrc file with correct permissions" {
export INPUT_GIT_IDENTITY="actions"
run _git_setup
[ "$status" -eq 0 ]
[ -f "$HOME/.netrc" ]
# Check file permissions (should be 600)
local perms=$(stat -c "%a" "$HOME/.netrc")
[ "$perms" = "600" ]
}
@test "_git_setup configures git with 'actions' identity" {
export INPUT_GIT_IDENTITY="actions"
run _git_setup
[ "$status" -eq 0 ]
# Check git config
local git_name=$(git config --global user.name)
local git_email=$(git config --global user.email)
[ "$git_name" = "GitHub Action" ]
[ "$git_email" = "actions@github.com" ]
}
@test "_git_setup configures git with 'author' identity" {
export INPUT_GIT_IDENTITY="author"
export GITHUB_ACTOR="test-user"
export GITHUB_ACTOR_ID="54321"
run _git_setup
[ "$status" -eq 0 ]
# Check git config
local git_name=$(git config --global user.name)
local git_email=$(git config --global user.email)
[ "$git_name" = "test-user" ]
[ "$git_email" = "54321+test-user@users.noreply.github.com" ]
}
@test "_git_setup fails with invalid identity" {
export INPUT_GIT_IDENTITY="invalid"
run _git_setup
[ "$status" -eq 1 ]
[[ "$output" =~ "GIT_IDENTITY must be either 'author' or 'actions'" ]]
}
@test "_git_changed returns true when files are modified" {
# Create and commit a file
echo "test" > test.txt
git add test.txt
git commit -m "Initial commit"
# Modify the file
echo "modified" > test.txt
run _git_changed
[ "$status" -eq 0 ]
}
@test "_git_changed returns false when no files are modified" {
# Create and commit a file
echo "test" > test.txt
git add test.txt
git commit -m "Initial commit"
# No modifications
run _git_changed
[ "$status" -eq 1 ]
}
@test "_git_changed returns true for untracked files" {
# Create a file without committing
echo "test" > untracked.txt
run _git_changed
[ "$status" -eq 0 ]
}
@test "_git_changed returns true for staged files" {
# Create and commit a file
echo "test" > test.txt
git add test.txt
git commit -m "Initial commit"
# Add a new file and stage it
echo "new file" > new.txt
git add new.txt
run _git_changed
[ "$status" -eq 0 ]
}