diff --git a/lib/diff b/lib/diff new file mode 100644 index 0000000..c6f3271 --- /dev/null +++ b/lib/diff @@ -0,0 +1,120 @@ +#! /usr/bin/env bash +# +# Tools for examining file differences +# +# Exports: +# @go.diff_check_editor +# Checks that `_GO_DIFF_EDITOR` is set +# +# @go.diff_files +# Log differences between two files +# +# @go.diff_directories +# Log differences between two directory structures + +export _GO_DIFF_EDITOR="${_GO_DIFF_EDITOR:-vimdiff}" + +. "$_GO_USE_MODULES" 'log' 'path' + +# Checks that `_GO_DIFF_EDITOR` is set +# +# Used by the functions in this module when the `--edit` flag is specified. +# Fails with a stack trace if it isn't set. +@go.diff_check_editor() { + if [[ -z "$_GO_DIFF_EDITOR" ]]; then + @go.log FATAL "_GO_DIFF_EDITOR not defined" + elif ! command -v "$_GO_DIFF_EDITOR" >/dev/null; then + @go.log FATAL "_GO_DIFF_EDITOR not installed: $_GO_DIFF_EDITOR" + fi +} + +# Log differences between two files +# +# Options: +# --edit: Open `_GO_DIFF_EDITOR` on the files if they differ +# +# Arguments: +# lhs: The "left-hand side" file +# rhs: The "right-hand side" file +@go.diff_files() { + local lhs + local rhs + local edit + + if [[ "$1" == '--edit' ]]; then + @go.diff_check_editor + edit='true' + shift + fi + + lhs="$1" + rhs="$2" + + if [[ ! -f "$lhs" ]]; then + @go.log WARN "Left-hand side file $1 doesn't exist or isn't a regular file" + elif [[ ! -f "$rhs" ]]; then + @go.log WARN "Right-hand side file $2 doesn't exist or isn't a regular file" + elif ! diff "$lhs" "$rhs" >/dev/null; then + @go.log WARN "$lhs" differs from "$rhs" + if [[ -n "$edit" ]]; then + @go.log INFO "Editing $lhs and $rhs" + "$_GO_DIFF_EDITOR" "$lhs" "$rhs" + fi + else + return '0' + fi + return '1' +} + +# Log differences between two directory structures +# +# Note that files and directories from `lhs_dir` that are missing in `rhs_dir` +# are reported, but not the other way around. +# +# Options: +# --edit: Open `_GO_DIFF_EDITOR` on any files that differ between the dirs +# +# Arguments: +# lhs_dir: The "left-hand side" directory +# rhs_dir: The "right-hand side" directory +@go.diff_directories() { + local __go_lhs_dir + local __go_rhs_dir + local __go_diff_files_args=() + local __go_diff_directories_result='0' + + if [[ "$1" == '--edit' ]]; then + __go_diff_files_args=('--edit') + shift + fi + + __go_lhs_dir="$1" + __go_rhs_dir="$2" + + if [[ ! -d "$__go_lhs_dir" ]]; then + @go.log WARN \ + "Left-hand side directory $1 doesn't exist or isn't a directory" + elif [[ ! -d "$__go_rhs_dir" ]]; then + @go.log WARN \ + "Right-hand side directory $2 doesn't exist or isn't a directory" + else + @go.walk_file_system _@go.diff_directories_impl "$__go_lhs_dir" || : + return "$__go_diff_directories_result" + fi + return '1' +} + +# -------------------------------- +# IMPLEMENTATION - HERE BE DRAGONS +# +# None of the functions below this line are part of the public interface. +# -------------------------------- +_@go.diff_directories_impl() { + local lhs="$1" + local rhs="${__go_rhs_dir}${lhs#$__go_lhs_dir}" + + if [[ -f "$lhs" ]] && + ! @go.diff_files "${__go_diff_files_args[@]}" "$lhs" "$rhs"; then + __go_diff_directories_result='1' + fi +} diff --git a/tests/diff/check-diff-editor.bats b/tests/diff/check-diff-editor.bats new file mode 100644 index 0000000..adc40cc --- /dev/null +++ b/tests/diff/check-diff-editor.bats @@ -0,0 +1,37 @@ +#! /usr/bin/env bats + +load ../environment + +setup() { + test_filter + @go.create_test_go_script \ + '. "$_GO_USE_MODULES" "diff"'\ + '@go.diff_check_editor' +} + +teardown() { + @go.remove_test_go_rootdir +} + +@test "$SUITE: fails if __GO_DIFF_EDITOR not defined" { + @go.create_test_go_script \ + '. "$_GO_USE_MODULES" "diff"'\ + '_GO_DIFF_EDITOR= @go.diff_check_editor' + run "$TEST_GO_SCRIPT" + assert_failure + assert_output_matches 'FATAL.* _GO_DIFF_EDITOR not defined' +} + +@test "$SUITE: fails if _GO_DIFF_EDITOR not found" { + _GO_DIFF_EDITOR='nonexistent-editor' run "$TEST_GO_SCRIPT" + assert_failure + assert_output_matches \ + 'FATAL.* _GO_DIFF_EDITOR not installed: nonexistent-editor' +} + +@test "$SUITE: does nothing if _GO_DIFF_EDITOR found" { + stub_program_in_path 'vimdiff' + _GO_DIFF_EDITOR='vimdiff' run "$TEST_GO_SCRIPT" + restore_program_in_path 'vimdiff' + assert_success '' +} diff --git a/tests/diff/diff-directories.bats b/tests/diff/diff-directories.bats new file mode 100644 index 0000000..1df1421 --- /dev/null +++ b/tests/diff/diff-directories.bats @@ -0,0 +1,107 @@ +#! /usr/bin/env bats + +load ../environment + +LHS_DIR= +RHS_DIR= + +setup() { + test_filter + @go.create_test_go_script \ + '. "$_GO_USE_MODULES" "diff"' \ + '@go.diff_directories "$@"' + + LHS_DIR="$TEST_GO_ROOTDIR/lhs" + RHS_DIR="$TEST_GO_ROOTDIR/rhs" + mkdir -p "$LHS_DIR" "$RHS_DIR" +} + +teardown() { + @go.remove_test_go_rootdir +} + +@test "$SUITE: warns if LHS dir doesn't exist" { + local lhs="$LHS_DIR" + rmdir "$lhs" + run "$TEST_GO_SCRIPT" "$lhs" "$RHS_DIR" + assert_failure + assert_output_matches \ + "WARN.* Left-hand side directory $lhs doesn't exist or isn't a directory" +} + +@test "$SUITE: warns if RHS dir doesn't exist" { + local rhs="$RHS_DIR" + rmdir "$rhs" + run "$TEST_GO_SCRIPT" "$LHS_DIR" "$rhs" + assert_failure + assert_output_matches \ + "WARN.* Right-hand side directory $rhs doesn't exist or isn't a directory" +} + +@test "$SUITE: does nothing if first directory empty" { + printf '%s\n' 'foo' >"$RHS_DIR/foo" + run "$TEST_GO_SCRIPT" "$LHS_DIR" "$RHS_DIR" + assert_success '' +} + +@test "$SUITE: returns success if directories contain the same single file" { + skip_if_system_missing 'diff' + printf '%s\n' 'foo' >"$LHS_DIR/foo" + printf '%s\n' 'foo' >"$RHS_DIR/foo" + run "$TEST_GO_SCRIPT" "$LHS_DIR" "$RHS_DIR" + assert_success '' +} + +@test "$SUITE: returns success if rhs contains more files" { + skip_if_system_missing 'diff' + printf '%s\n' 'foo' >"$LHS_DIR/foo" + printf '%s\n' 'foo' >"$RHS_DIR/foo" + printf '%s\n' 'bar' >"$RHS_DIR/bar" + run "$TEST_GO_SCRIPT" "$LHS_DIR" "$RHS_DIR" + assert_success '' +} + +@test "$SUITE: returns an error if rhs contains fewer files" { + skip_if_system_missing 'diff' + local missing="$RHS_DIR/bar" + + printf '%s\n' 'foo' >"$LHS_DIR/foo" + printf '%s\n' 'bar' >"$LHS_DIR/bar" + printf '%s\n' 'foo' >"$RHS_DIR/foo" + + run "$TEST_GO_SCRIPT" "$LHS_DIR" "$RHS_DIR" + assert_failure + assert_output_matches \ + "WARN.* Right-hand side file $missing doesn't exist or isn't a regular file" +} + +@test "$SUITE: warns and returns an error when files differ" { + skip_if_system_missing 'diff' + + printf '%s\n' 'foo' >"$LHS_DIR/foo" + printf '%s\n' 'bar' >"$RHS_DIR/foo" + + run "$TEST_GO_SCRIPT" "$LHS_DIR" "$RHS_DIR" + assert_failure + assert_output_matches "WARN.* $LHS_DIR/foo differs from $RHS_DIR/foo" +} + +@test "$SUITE: --edit opens _GO_DIFF_EDITOR when files differ" { + skip_if_system_missing 'diff' + + printf '%s\n' 'foo' >"$LHS_DIR/foo" + printf '%s\n' 'bar' >"$RHS_DIR/foo" + + stub_program_in_path 'vimdiff' \ + 'printf "%s\n" "LHS: $1" "RHS: $2"' + + _GO_DIFF_EDITOR='vimdiff' run "$TEST_GO_SCRIPT" --edit "$LHS_DIR" "$RHS_DIR" + restore_program_in_path 'vimdiff' + + assert_failure + assert_lines_match \ + "WARN.* $LHS_DIR/foo differs from $RHS_DIR/foo" \ + "INFO.* Editing $LHS_DIR/foo and $RHS_DIR/foo" \ + "LHS: $LHS_DIR/foo" \ + "RHS: $RHS_DIR/foo" +} diff --git a/tests/diff/diff-files.bats b/tests/diff/diff-files.bats new file mode 100644 index 0000000..77976bc --- /dev/null +++ b/tests/diff/diff-files.bats @@ -0,0 +1,90 @@ +#! /usr/bin/env bats + +load ../environment + +setup() { + test_filter + @go.create_test_go_script \ + '. "$_GO_USE_MODULES" "diff"'\ + '@go.diff_files "$@"' +} + +teardown() { + @go.remove_test_go_rootdir +} + +@test "$SUITE: warns if lhs file doesn't exist or isn't regular" { + local lhs="$TEST_GO_ROOTDIR/foo" + + run "$TEST_GO_SCRIPT" "$lhs" + assert_failure + assert_output_matches \ + "WARN.* Left-hand side file $lhs doesn't exist or isn't a regular file" +} + +@test "$SUITE: warns if rhs file doesn't exist or isn't regular" { + local lhs="$TEST_GO_ROOTDIR/foo" + local rhs="$TEST_GO_ROOTDIR/bar" + + mkdir -p "$TEST_GO_ROOTDIR" + printf '%s\n' 'foo' >"$lhs" + mkdir "$rhs" + + run "$TEST_GO_SCRIPT" "$lhs" "$rhs" + assert_failure + assert_output_matches \ + "WARN.* Right-hand side file $rhs doesn't exist or isn't a regular file" +} + +@test "$SUITE: return success if the files match" { + skip_if_system_missing 'diff' + + local lhs="$TEST_GO_ROOTDIR/foo" + local rhs="$TEST_GO_ROOTDIR/bar" + + mkdir -p "$TEST_GO_ROOTDIR" + printf '%s\n' 'foo' >"$lhs" + printf '%s\n' 'foo' >"$rhs" + + run "$TEST_GO_SCRIPT" "$lhs" "$rhs" + assert_success '' +} + +@test "$SUITE: warns and return failure if the files differ" { + skip_if_system_missing 'diff' + + local lhs="$TEST_GO_ROOTDIR/foo" + local rhs="$TEST_GO_ROOTDIR/bar" + + mkdir -p "$TEST_GO_ROOTDIR" + printf '%s\n' 'foo' >"$lhs" + printf '%s\n' 'bar' >"$rhs" + + run "$TEST_GO_SCRIPT" "$lhs" "$rhs" + assert_failure + assert_output_matches "WARN.* $lhs differs from $rhs" +} + +@test "$SUITE: --edit opens _GO_DIFF_EDITOR if the files differ" { + skip_if_system_missing 'diff' + + local lhs="$TEST_GO_ROOTDIR/foo" + local rhs="$TEST_GO_ROOTDIR/bar" + + mkdir -p "$TEST_GO_ROOTDIR" + printf '%s\n' 'foo' >"$lhs" + printf '%s\n' 'bar' >"$rhs" + + stub_program_in_path 'vimdiff' \ + 'printf "%s\n" "LHS: $1" "RHS: $2"' + + _GO_DIFF_EDITOR='vimdiff' run "$TEST_GO_SCRIPT" --edit "$lhs" "$rhs" + restore_program_in_path 'vimdiff' + + assert_failure + assert_lines_match \ + "WARN.* $lhs differs from $rhs" \ + "INFO.* Editing $lhs and $rhs" \ + "LHS: $lhs" \ + "RHS: $rhs" +}