diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml new file mode 100644 index 0000000..f090d32 --- /dev/null +++ b/.github/workflows/verify.yml @@ -0,0 +1,27 @@ +name: Verify + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Build generated scripts + run: make -B all + + - name: Run verification + run: make verify + + - name: Check for generated diffs + run: git diff --exit-code diff --git a/Makefile b/Makefile index 897bfde..33f5da1 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,12 @@ SOURCES = bj.sh rollup.rb linebreak.rb TARGETS = bj-1line.sh bj-80-col.sh bj-90-col.sh -.PHONY: all +.PHONY: all verify all: $(TARGETS) +verify: + ./verify.sh + $(TARGETS): $(SOURCES) bj-1line.sh: diff --git a/README.md b/README.md index 4820798..c4d0eed 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Yes, this bizarre 863-byte Bash JSON parser is actually tested.](https://github.com/memotype/bj.sh/actions/workflows/verify.yml/badge.svg)](https://github.com/memotype/bj.sh/actions/workflows/verify.yml) + bj.sh is a pure GNU Bash library for parsing JSON data. bj.sh is meant to be run as a script, sourced as a library, or copied directly diff --git a/verify.sh b/verify.sh new file mode 100755 index 0000000..5aa1880 --- /dev/null +++ b/verify.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash + +cd -- "$(dirname -- "${BASH_SOURCE[0]}")" || exit 1 + +fail() { + echo "VERIFY FAIL: $*" >&2 + exit 1 +} + +generated=(bj-1line.sh bj-80-col.sh bj-90-col.sh) +implementations=(bj.sh "${generated[@]}") +executables=(bj.sh test.sh verify.sh rollup.rb linebreak.rb "${generated[@]}") + +echo "Checking syntax and executable modes" +bash -n bj.sh test.sh verify.sh "${generated[@]}" \ + || fail "Bash syntax check failed" +ruby -c rollup.rb || fail "rollup.rb syntax check failed" +ruby -c linebreak.rb || fail "linebreak.rb syntax check failed" + +for file in "${executables[@]}"; do + [[ -x $file ]] || fail "$file is not executable" +done + +echo "Checking bj.sh CLI" +output=$(./bj.sh '{"foo":"bar"}' foo) +status=$? +[[ $output = bar && $status = 0 ]] || fail "bj.sh CLI query failed" + +output=$(printf %s '{"foo":"bar"}' | ./bj.sh - foo) +status=$? +[[ $output = bar && $status = 0 ]] || fail "bj.sh CLI stdin query failed" + +output=$(./bj.sh '{"foo":"bar"}' missing) +status=$? +[[ -z $output && $status = 1 ]] \ + || fail "bj.sh CLI missing-query status failed" + +echo "Checking generated files" +verify_dir=$(mktemp -d) || fail "Could not create temporary directory" +trap 'rm -r -- "$verify_dir"' EXIT + +./rollup.rb bj.sh "$verify_dir/bj-1line.sh" \ + || fail "Could not regenerate bj-1line.sh" +./linebreak.rb --max-lines 13 80 \ + "$verify_dir/bj-1line.sh" "$verify_dir/bj-80-col.sh" \ + || fail "Could not regenerate bj-80-col.sh" +./linebreak.rb --max-lines 12 90 \ + "$verify_dir/bj-1line.sh" "$verify_dir/bj-90-col.sh" \ + || fail "Could not regenerate bj-90-col.sh" + +for file in "${generated[@]}"; do + diff -u "$file" "$verify_dir/$file" \ + || fail "$file differs from regenerated output" +done + +awk '!/^#/ { code++ } END { + if (NR != 2 || code != 1) { + print FILENAME ": expected one header and one code line" > "/dev/stderr" + exit 1 + } +}' bj-1line.sh || fail "bj-1line.sh is not the canonical one-line form" + +check_wrapped() { + local file=$1 width=$2 max_lines=$3 + + awk -v width="$width" -v max_lines="$max_lines" ' + length > width { + printf "%s:%d exceeds %d columns (%d)\n", + FILENAME, FNR, width, length($0) > "/dev/stderr" + bad=1 + } + !/^#/ { code++ } + END { + if (code > max_lines) { + printf "%s has %d code lines; maximum is %d\n", + FILENAME, code, max_lines > "/dev/stderr" + bad=1 + } + exit bad + } + ' "$file" || fail "$file violates its wrapping constraints" +} + +check_wrapped bj-80-col.sh 80 13 +check_wrapped bj-90-col.sh 90 12 + +run_functional_tests() { + local label=$1 + shift + + echo "Functional tests ($label)" + for implementation in "${implementations[@]}"; do + "$@" ./test.sh -s "./$implementation" \ + || fail "$implementation failed under $label" + done +} + +run_functional_tests "the default locale" +run_functional_tests "LC_ALL=C" env LC_ALL=C + +echo "Checking diffs" +git diff --check || fail "Unstaged changes contain whitespace errors" +git diff --cached --check || fail "Staged changes contain whitespace errors" + +echo "Verification passed"