From e1aa25db96148cbf058c4ff848738fe917c27932 Mon Sep 17 00:00:00 2001 From: Nicolas Chan Date: Wed, 3 Mar 2021 17:50:04 -0800 Subject: [PATCH] Add unit tests --- .gitignore | 1 + .gitmodules | 3 + backup.sh | 27 ++-- test.sh | 18 --- test/.gitignore | 1 + test/data/test-chat-messages.txt | 5 + test/data/test-chat-prefix.txt | 5 + test/shunit2 | 1 + test/test.sh | 224 +++++++++++++++++++++++++++++++ 9 files changed, 256 insertions(+), 29 deletions(-) create mode 100644 .gitignore create mode 100644 .gitmodules delete mode 100755 test.sh create mode 100644 test/.gitignore create mode 100644 test/data/test-chat-messages.txt create mode 100644 test/data/test-chat-prefix.txt create mode 160000 test/shunit2 create mode 100755 test/test.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ebc8ae --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +coverage diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..587f9be --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "test/shunit2"] + path = test/shunit2 + url = https://github.com/kward/shunit2.git diff --git a/backup.sh b/backup.sh index 6db5a7b..afada59 100755 --- a/backup.sh +++ b/backup.sh @@ -1,7 +1,7 @@ -#!/bin/bash +#!/usr/bin/env bash # Minecraft server automatic backup management script -# by Nicolas Chan +# https://github.com/nicolaschan/minecraft-backup # MIT License # # For Minecraft servers running in a GNU screen. @@ -33,7 +33,8 @@ while getopts 'a:cd:e:f:hi:l:m:o:p:qs:vw:' FLAG; do d) DELETE_METHOD=$OPTARG ;; e) COMPRESSION_FILE_EXTENSION=".$OPTARG" ;; f) TIMESTAMP=$OPTARG ;; - h) echo "Minecraft Backup (by Nicolas Chan)" + h) echo "Minecraft Backup" + echo "Repository: https://github.com/nicolaschan/minecraft-backup" echo "-a Compression algorithm (default: gzip)" echo "-c Enable chat messages" echo "-d Delete method: thin (default), sequential, none" @@ -70,6 +71,10 @@ log-warning () { echo -e "\033[0;33mWARNING:\033[0m $*" } +if [[ $COMPRESSION_FILE_EXTENSION == "." ]]; then + COMPRESSION_FILE_EXTENSION="" +fi + # Check for missing encouraged arguments if ! $SUPPRESS_WARNINGS; then if [[ $SCREEN_NAME == "" ]]; then @@ -88,7 +93,7 @@ if [[ $BACKUP_DIRECTORY == "" ]]; then fi if $MISSING_CONFIGURATION; then - exit 0 + exit 1 fi ARCHIVE_FILE_NAME=$TIMESTAMP.tar$COMPRESSION_FILE_EXTENSION @@ -239,11 +244,11 @@ execute-command "save-off" # Backup world START_TIME=$(date +"%s") case $COMPRESSION_ALGORITHM in - "") # No compression - tar -cf $ARCHIVE_PATH -C $SERVER_WORLD . + # No compression + "") tar -cf $ARCHIVE_PATH -C $SERVER_WORLD . ;; - *) # With compression - tar -cf - -C $SERVER_WORLD . | $COMPRESSION_ALGORITHM -cv -$COMPRESSION_LEVEL - > $ARCHIVE_PATH 2>> /dev/null + # With compression + *) tar -cf - -C $SERVER_WORLD . | $COMPRESSION_ALGORITHM -cv -$COMPRESSION_LEVEL - > $ARCHIVE_PATH 2>> /dev/null ;; esac sync @@ -268,13 +273,13 @@ delete-old-backups () { # Notify players of completion WORLD_SIZE_BYTES=$(du -b --max-depth=0 $SERVER_WORLD | awk '{print $1}') ARCHIVE_SIZE_BYTES=$(du -b $ARCHIVE_PATH | awk '{print $1}') -COMPRESSION_PERCENT=$(($ARCHIVE_SIZE_BYTES * 100 / $WORLD_SIZE_BYTES)) ARCHIVE_SIZE=$(du -h $ARCHIVE_PATH | awk '{print $1}') BACKUP_DIRECTORY_SIZE=$(du -h --max-depth=0 $BACKUP_DIRECTORY | awk '{print $1}') TIME_DELTA=$((END_TIME - START_TIME)) -# Check that archive size is not null and at least 1024 KB -if [[ "$ARCHIVE_SIZE" != "" && "$ARCHIVE_SIZE_BYTES" -gt 8 ]]; then +# Check that archive size is not null and at least 200 Bytes +if [[ "$WORLD_SIZE_BYTES" -gt 0 && "$ARCHIVE_SIZE" != "" && "$ARCHIVE_SIZE_BYTES" -gt 200 ]]; then + COMPRESSION_PERCENT=$(($ARCHIVE_SIZE_BYTES * 100 / $WORLD_SIZE_BYTES)) message-players-success "Backup complete!" "$TIME_DELTA s, $ARCHIVE_SIZE/$BACKUP_DIRECTORY_SIZE, $COMPRESSION_PERCENT%" delete-old-backups else diff --git a/test.sh b/test.sh deleted file mode 100755 index 4d54554..0000000 --- a/test.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -START_TIMESTAMP=1501484400 - -ITERATIONS=1000 -MINUTE_INTERVAL=30 -MINUTES_SINCE_START=0 - -if [[ $1 != "" ]]; then - ITERATIONS=$1 -fi - -for (( c=1; c<=$ITERATIONS; c++ )); do - TIMESTAMP=$(( START_TIMESTAMP + MINUTES_SINCE_START * 60 )) - FILE_NAME=$(date -d "@$TIMESTAMP" +%F_%H-%M-%S) - ./backup.sh -q -i /home/nicolas/privatesurvival/world -o /home/nicolas/backups -f $FILE_NAME - (( MINUTES_SINCE_START += MINUTE_INTERVAL )) -done diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 0000000..a9a5aec --- /dev/null +++ b/test/.gitignore @@ -0,0 +1 @@ +tmp diff --git a/test/data/test-chat-messages.txt b/test/data/test-chat-messages.txt new file mode 100644 index 0000000..74efae3 --- /dev/null +++ b/test/data/test-chat-messages.txt @@ -0,0 +1,5 @@ +tellraw @a ["",{"text":"[Backup] ","color":"gray","italic":true},{"text":"Starting backup...","color":"gray","italic":true,"hoverEvent":{"action":"show_text","value":{"text":"","extra":[{"text":"2021-01-01_00-00-00.tar.gz"}]}}}] +save-off +save-on +save-all +tellraw @a ["",{"text":"[Backup] ","color":"gray","italic":true},{"text":"Backup complete!","color":"green","italic":true,"hoverEvent":{"action":"show_text","value":{"text":"","extra":[{"text":"0 s, 4.0K/4.0K, 316%"}]}}}] diff --git a/test/data/test-chat-prefix.txt b/test/data/test-chat-prefix.txt new file mode 100644 index 0000000..88346f7 --- /dev/null +++ b/test/data/test-chat-prefix.txt @@ -0,0 +1,5 @@ +tellraw @a ["",{"text":"[Hello] ","color":"gray","italic":true},{"text":"Starting backup...","color":"gray","italic":true,"hoverEvent":{"action":"show_text","value":{"text":"","extra":[{"text":"2021-01-01_00-00-00.tar.gz"}]}}}] +save-off +save-on +save-all +tellraw @a ["",{"text":"[Hello] ","color":"gray","italic":true},{"text":"Hello complete!","color":"green","italic":true,"hoverEvent":{"action":"show_text","value":{"text":"","extra":[{"text":"0 s, 4.0K/4.0K, 316%"}]}}}] diff --git a/test/shunit2 b/test/shunit2 new file mode 160000 index 0000000..ebc4baa --- /dev/null +++ b/test/shunit2 @@ -0,0 +1 @@ +Subproject commit ebc4baa08f045b7ef0f45c4b7d6f34f08d732f3d diff --git a/test/test.sh b/test/test.sh new file mode 100755 index 0000000..7e3e0c9 --- /dev/null +++ b/test/test.sh @@ -0,0 +1,224 @@ +#!/usr/bin/env bash + +# Helper functions + +TEST_DIR="test" +TEST_TMP="$TEST_DIR/tmp" +SCREEN_TMP="tmp-screen" +setUp () { + rm -rf "$TEST_TMP" + mkdir -p "$TEST_TMP/server/world" + mkdir -p "$TEST_TMP/backups" + echo "file1" > "$TEST_TMP/server/world/file1.txt" + echo "file2" > "$TEST_TMP/server/world/file2.txt" + echo "file3" > "$TEST_TMP/server/world/file3.txt" + + screen -dmS "$SCREEN_TMP" bash + screen -S "$SCREEN_TMP" -X stuff "cat > $TEST_TMP/screen-output\n" + tmux new-session -d -s "$SCREEN_TMP" + tmux send-keys -t "$SCREEN_TMP" "cat > $TEST_TMP/tmux-output" ENTER + sleep 0.5 +} + +tearDown () { + screen -S "$SCREEN_TMP" -X quit >/dev/null 2>&1 || true + tmux kill-session -t "$SCREEN_TMP" >/dev/null 2>&1 || true +} + +assert-equals-directory () { + if [ -d "$1" ]; then + for FILE in "$1"/*; do + assert-equals-directory "$FILE" "$2/${FILE##$1}" + done + else + assertEquals "$(cat "$1")" "$(cat "$2")" + fi +} + +check-backup () { + BACKUP_ARCHIVE="$1" + mkdir -p "$TEST_TMP/restored" + tar --extract --file "$TEST_TMP/backups/$BACKUP_ARCHIVE" --directory "$TEST_TMP/restored" + assert-equals-directory "$TEST_TMP/server/world" "$TEST_TMP/restored" + rm -rf "$TEST_TMP/restored" +} + +# Tests + +test-backup-defaults () { + TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01")" + ./backup.sh -i "$TEST_TMP/server/world" -o "$TEST_TMP/backups" -s "$SCREEN_TMP" -f "$TIMESTAMP" + check-backup "$TIMESTAMP.tar.gz" +} + +test-backup-no-compression () { + TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01")" + ./backup.sh -a "" -e "" -i "$TEST_TMP/server/world" -o "$TEST_TMP/backups" -s "$SCREEN_TMP" -f "$TIMESTAMP" + check-backup "$TIMESTAMP.tar" +} + +test-backup-max-compression () { + TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01")" + ./backup.sh -a "xz" -e "xz" -l 9e -i "$TEST_TMP/server/world" -o "$TEST_TMP/backups" -s "$SCREEN_TMP" -f "$TIMESTAMP" + check-backup "$TIMESTAMP.tar.xz" +} + +test-chat-messages () { + TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01")" + ./backup.sh -c -i "$TEST_TMP/server/world" -o "$TEST_TMP/backups" -s "$SCREEN_TMP" -f "$TIMESTAMP" + EXPECTED_OUTPUT="$(head -n-1 "$TEST_DIR/data/test-chat-messages.txt")" + ACTUAL_OUTPUT="$(head -n-1 "$TEST_TMP/screen-output")" + assertEquals "$EXPECTED_OUTPUT" "$ACTUAL_OUTPUT" +} + +test-chat-prefix () { + TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01")" + ./backup.sh -p "Hello" -c -i "$TEST_TMP/server/world" -o "$TEST_TMP/backups" -s "$SCREEN_TMP" -f "$TIMESTAMP" + EXPECTED_OUTPUT="$(head -n-1 "$TEST_DIR/data/test-chat-prefix.txt")" + ACTUAL_OUTPUT="$(head -n-1 "$TEST_TMP/screen-output")" + assertEquals "$EXPECTED_OUTPUT" "$ACTUAL_OUTPUT" +} + +test-check-help () { + HELP_HEADER="$(./backup.sh -h)" + assertEquals "Minecraft Backup" "$(head -n1 <<< "$HELP_HEADER")" +} + +test-missing-options () { + OUTPUT="$(./backup.sh 2>&1)" + EXIT_CODE="$?" + assertEquals 1 "$EXIT_CODE" + assertContains "$OUTPUT" "Minecraft screen name not specified" + assertContains "$OUTPUT" "Server world not specified" + assertContains "$OUTPUT" "Backup directory not specified" +} + +test-missing-options-suppress-warnings () { + OUTPUT="$(./backup.sh -q 2>&1)" + EXIT_CODE="$?" + assertEquals 1 "$EXIT_CODE" + assertNotContains "$OUTPUT" "Minecraft screen name not specified" +} + +test-empty-world-warning () { + mkdir -p "$TEST_TMP/server/empty-world" + OUTPUT="$(./backup.sh -v -i "$TEST_TMP/server/empty-world" -o "$TEST_TMP/backups" -s "$SCREEN_TMP" -f "$TIMESTAMP" 2>&1)" + assertContains "$OUTPUT" "Backup was not saved!" +} + +test-block-size-warning () { + TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01")" + OUTPUT="$(./backup.sh -m 10 -i "$TEST_TMP/server/world" -o "$TEST_TMP/backups" -s "$SCREEN_TMP" -f "$TIMESTAMP" 2>&1)" + EXIT_CODE="$?" + assertContains "$OUTPUT" "is smaller than TOTAL_BLOCK_SIZE" +} + +test-screen-interface () { + TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01")" + ./backup.sh -i "$TEST_TMP/server/world" -o "$TEST_TMP/backups" -s "$SCREEN_TMP" -f "$TIMESTAMP" + EXPECTED_CONTENTS=$(echo -e "save-off\nsave-on\nsave-all") + SCREEN_CONTENTS="$(cat "$TEST_TMP/screen-output")" + assertEquals "$SCREEN_CONTENTS" "$EXPECTED_CONTENTS" +} + +test-tmux-interface () { + TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01")" + ./backup.sh -w tmux -i "$TEST_TMP/server/world" -o "$TEST_TMP/backups" -s "$SCREEN_TMP" -f "$TIMESTAMP" + EXPECTED_CONTENTS=$(echo -e "save-off\nsave-on\nsave-all") + SCREEN_CONTENTS="$(cat "$TEST_TMP/tmux-output")" + assertEquals "$SCREEN_CONTENTS" "$EXPECTED_CONTENTS" +} + +test-sequential-delete () { + for i in $(seq 0 99); do + TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01 +$i hour")" + ./backup.sh -d "sequential" -m 10 -i "$TEST_TMP/server/world" -o "$TEST_TMP/backups" -s "$SCREEN_TMP" -f "$TIMESTAMP" + done + for i in $(seq 90 99); do + TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01 +$i hour")" + check-backup "$TIMESTAMP.tar.gz" + done +} + +test-thinning-delete () { + for i in $(seq 0 99); do + TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01 +$i hour")" + ./backup.sh -i "$TEST_TMP/server/world" -o "$TEST_TMP/backups" -s "$SCREEN_TMP" -f "$TIMESTAMP" + done + EXPECTED_TIMESTAMPS=( + # Weekly + + # Daily (30) + "2021-01-01_00-00-00" + "2021-01-02_00-00-00" + "2021-01-03_00-00-00" + + # Hourly (24) + "2021-01-03_12-00-00" + "2021-01-03_13-00-00" + "2021-01-03_14-00-00" + "2021-01-03_15-00-00" + "2021-01-03_16-00-00" + "2021-01-03_17-00-00" + "2021-01-03_18-00-00" + "2021-01-03_19-00-00" + "2021-01-03_20-00-00" + "2021-01-04_09-00-00" + "2021-01-04_10-00-00" + "2021-01-04_11-00-00" + + # Sub-hourly (16) + "2021-01-04_12-00-00" + "2021-01-04_13-00-00" + "2021-01-04_14-00-00" + "2021-01-04_15-00-00" + "2021-01-04_16-00-00" + "2021-01-04_17-00-00" + "2021-01-04_18-00-00" + "2021-01-04_19-00-00" + "2021-01-04_20-00-00" + "2021-01-04_21-00-00" + "2021-01-04_22-00-00" + "2021-01-04_23-00-00" + "2021-01-05_00-00-00" + "2021-01-05_01-00-00" + "2021-01-05_02-00-00" + "2021-01-05_03-00-00" + ) + for TIMESTAMP in "${EXPECTED_TIMESTAMPS[@]}"; do + check-backup "$TIMESTAMP.tar.gz" + done +} + +test-thinning-delete-long () { + for i in $(seq 0 99); do + TIMESTAMP="$(date +%F_%H-%M-%S --date="2021-01-01 +$i day")" + OUTPUT="$(./backup.sh -v -i "$TEST_TMP/server/world" -o "$TEST_TMP/backups" -s "$SCREEN_TMP" -f "$TIMESTAMP")" + done + EXPECTED_TIMESTAMPS=( + # Weekly + "2021-01-04_00-00-00" + "2021-01-11_00-00-00" + "2021-01-25_00-00-00" + "2021-01-25_00-00-00" + + # Daily (30) + "2021-01-31_00-00-00" + "2021-03-01_00-00-00" + + # Hourly (24) + "2021-03-02_00-00-00" + "2021-03-25_00-00-00" + + # Sub-hourly (16) + "2021-03-26_00-00-00" + "2021-04-10_00-00-00" + ) + assertContains "$OUTPUT" "promoted to next block" + for TIMESTAMP in "${EXPECTED_TIMESTAMPS[@]}"; do + check-backup "$TIMESTAMP.tar.gz" + done +} + +# shellcheck disable=SC1091 +. test/shunit2/shunit2