#!/bin/bash
# SPDX-License-Identifier: MIT
# Copyright 2020 Google LLC
#
# Use of this source code is governed by an MIT-style
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.
#
#
# Test script for fsverity-utils.  Runs 'make check' in lots of configurations,
# runs static analysis, and does a few other tests.
#
# Note: for more test coverage, in addition to running this script, also build
# fsverity-utils into a kvm-xfstests test appliance and run
# 'kvm-xfstests -c ext4,f2fs -g verity'

set -e -u -o pipefail
cd "$(dirname "$0")/.."

log()
{
	echo "[$(date)] $*" 1>&2
}

fail()
{
	echo "FAIL: $*" 1>&2
	exit 1
}

TMPDIR=$(mktemp -d -t libfsverity_test.XXXXXXXXX)
trap 'rm -r "$TMPDIR"' EXIT

# Both stdout and stderr go to log file.
# Only stderr goes to terminal.
echo "Starting fsverity-utils tests.  See run-tests.log for full output."
rm -f run-tests.log
exec >> run-tests.log
exec 2> >(tee -ia run-tests.log 1>&2)

MAKE="make -j$(getconf _NPROCESSORS_ONLN)"

TEST_FUNCS=()

static_linking_test()
{
	log "Build and test with statically linking"
	$MAKE CFLAGS="-Werror"
	if ldd fsverity | grep libfsverity.so; then
		fail "fsverity binary should be statically linked to libfsverity by default"
	fi
	./fsverity --version

	log "Check that all global symbols are prefixed with \"libfsverity_\""
	if nm libfsverity.a | grep ' T ' | grep -v " libfsverity_"; then
		fail "Some global symbols are not prefixed with \"libfsverity_\""
	fi
}
TEST_FUNCS+=(static_linking_test)

dynamic_linking_test()
{
	log "Build and test with dynamic linking"
	$MAKE CFLAGS="-Werror" USE_SHARED_LIB=1 check
	if ! ldd fsverity | grep libfsverity.so; then
		fail "fsverity binary should be dynamically linked to libfsverity when USE_SHARED_LIB=1"
	fi

	log "Check that all exported symbols are prefixed with \"libfsverity_\""
	if nm libfsverity.so | grep ' T ' | grep -v " libfsverity_"; then
		fail "Some exported symbols are not prefixed with \"libfsverity_\""
	fi
}
TEST_FUNCS+=(dynamic_linking_test)

cplusplus_test()
{
	$MAKE CFLAGS="-Werror" libfsverity.so
	log "Test using libfsverity from C++ program"
	cat > "$TMPDIR/test.cc" <<EOF
#include <libfsverity.h>
#include <iostream>
int main()
{
	std::cout << libfsverity_get_digest_size(FS_VERITY_HASH_ALG_SHA256) << std::endl;
}
EOF
	c++ -Wall -Werror "$TMPDIR/test.cc" -Iinclude -L. -lfsverity -o "$TMPDIR/test"
	[ "$(LD_LIBRARY_PATH=. "$TMPDIR/test")" = "32" ]
	rm "${TMPDIR:?}"/*
}
TEST_FUNCS+=(cplusplus_test)

untracked_files_test()
{
	log "Check that build doesn't produce untracked files"
	$MAKE CFLAGS="-Werror" all test_programs
	if git status --short | grep -q '^??'; then
		git status
		fail "Build produced untracked files (check 'git status').  Missing gitignore entry?"
	fi
}
TEST_FUNCS+=(untracked_files_test)

uninstall_test()
{
	log "Test that 'make uninstall' uninstalls all files"
	make DESTDIR="$TMPDIR" install
	if [ "$(find "$TMPDIR" -type f -o -type l | wc -l)" = 0 ]; then
		fail "'make install' didn't install any files"
	fi
	make DESTDIR="$TMPDIR" uninstall
	if [ "$(find "$TMPDIR" -type f -o -type l | wc -l)" != 0 ]; then
		fail "'make uninstall' didn't uninstall all files"
	fi
	rm -r "${TMPDIR:?}"/*
}
TEST_FUNCS+=(uninstall_test)

dash_test()
{
	log "Build, install, and uninstall with dash"
	make clean SHELL=/bin/dash
	make DESTDIR="$TMPDIR" SHELL=/bin/dash install
	make DESTDIR="$TMPDIR" SHELL=/bin/dash uninstall
}
TEST_FUNCS+=(dash_test)

license_test()
{
	log "Check that all files have license and copyright info"
	list="$TMPDIR/filelist"
	filter_license_info() {
		# files to exclude from license and copyright info checks
		grep -E -v '(\.gitignore|LICENSE|.*\.md|testdata|fsverity_uapi\.h|libfsverity\.pc\.in)'
	}
	git grep -L 'SPDX-License-Identifier: MIT' \
		| filter_license_info > "$list" || true
	if [ -s "$list" ]; then
		fail "The following files are missing an appropriate SPDX license identifier: $(<"$list")"
	fi
	# For now some people still prefer a free-form license statement, not just SPDX.
	git grep -L 'Use of this source code is governed by an MIT-style' \
		| filter_license_info > "$list" || true
	if [ -s "$list" ]; then
		fail "The following files are missing an appropriate license statement: $(<"$list")"
	fi
	git grep -L '\<Copyright\>' | filter_license_info > "$list" || true
	if [ -s "$list" ]; then
		fail "The following files are missing a copyright statement: $(<"$list")"
	fi
	rm "$list"
}
TEST_FUNCS+=(license_test)

gcc_test()
{
	log "Build and test with gcc (-O2)"
	$MAKE CC=gcc CFLAGS="-O2 -Werror" check

	log "Build and test with gcc (-O3)"
	$MAKE CC=gcc CFLAGS="-O3 -Werror" check
}
TEST_FUNCS+=(gcc_test)

clang_test()
{
	log "Build and test with clang (-O2)"
	$MAKE CC=clang CFLAGS="-O2 -Werror" check

	log "Build and test with clang (-O3)"
	$MAKE CC=clang CFLAGS="-O3 -Werror" check
}
TEST_FUNCS+=(clang_test)

32bit_test()
{
	log "Build and test with gcc (32-bit)"
	$MAKE CC=gcc CFLAGS="-O2 -Werror -m32" check
}
TEST_FUNCS+=(32bit_test)

sanitizers_test()
{
	log "Build and test with clang + UBSAN"
	$MAKE CC=clang \
		CFLAGS="-O2 -Werror -fsanitize=undefined -fno-sanitize-recover=undefined" \
		check

	log "Build and test with clang + ASAN"
	$MAKE CC=clang \
		CFLAGS="-O2 -Werror -fsanitize=address -fno-sanitize-recover=address" \
		check

	log "Build and test with clang + unsigned integer overflow sanitizer"
	$MAKE CC=clang \
		CFLAGS="-O2 -Werror -fsanitize=unsigned-integer-overflow -fno-sanitize-recover=unsigned-integer-overflow" \
		check

	log "Build and test with clang + CFI"
	$MAKE CC=clang CFLAGS="-O2 -Werror -fsanitize=cfi -flto -fvisibility=hidden" \
		AR=llvm-ar check
}
TEST_FUNCS+=(sanitizers_test)

valgrind_test()
{
	log "Build and test with valgrind"
	$MAKE TEST_WRAPPER_PROG="valgrind --quiet --error-exitcode=100 --leak-check=full --errors-for-leak-kinds=definite,possible" \
		CFLAGS="-O2 -Werror" check
}
TEST_FUNCS+=(valgrind_test)

boringssl_test()
{
	log "Build and test using BoringSSL instead of OpenSSL"
	log "-> Building BoringSSL"
	$MAKE boringssl
	log "-> Building fsverity-utils linked to BoringSSL"
	$MAKE CFLAGS="-O2 -Werror" \
		LDFLAGS="-Lboringssl/build -Wl,-rpath=$PWD/boringssl/build" \
		CPPFLAGS="-Iboringssl/include" LDLIBS="-lcrypto -lpthread" check
}
TEST_FUNCS+=(boringssl_test)

openssl1_test()
{
	log "Build and test using OpenSSL 1.0"
	$MAKE CFLAGS="-O2 -Werror" LDFLAGS="-L/usr/lib/openssl-1.0" \
		CPPFLAGS="-I/usr/include/openssl-1.0" check
}
TEST_FUNCS+=(openssl1_test)

openssl3_test()
{
	log "Build and test using OpenSSL 3.0"
	OSSL3=$HOME/src/openssl/inst/usr/local
	LD_LIBRARY_PATH="$OSSL3/lib64" $MAKE CFLAGS="-O2 -Werror" \
		LDFLAGS="-L$OSSL3/lib64" CPPFLAGS="-I$OSSL3/include" check
}
TEST_FUNCS+=(openssl3_test)

unsigned_char_test()
{
	log "Build and test using -funsigned-char"
	$MAKE CFLAGS="-O2 -Werror -funsigned-char" check
}
TEST_FUNCS+=(unsigned_char_test)

signed_char_test()
{
	log "Build and test using -fsigned-char"
	$MAKE CFLAGS="-O2 -Werror -fsigned-char" check
}
TEST_FUNCS+=(signed_char_test)

windows_build_test()
{
	log "Cross-compile for Windows (32-bit)"
	$MAKE CC=i686-w64-mingw32-gcc CFLAGS="-O2 -Werror"

	log "Cross-compile for Windows (64-bit)"
	$MAKE CC=x86_64-w64-mingw32-gcc CFLAGS="-O2 -Werror"
}
TEST_FUNCS+=(windows_build_test)

sparse_test()
{
	log "Run sparse"
	./scripts/run-sparse.sh
}
TEST_FUNCS+=(sparse_test)

clang_analyzer_test()
{
	log "Run clang static analyzer"
	scan-build --status-bugs make CFLAGS="-O2 -Werror" all test_programs
}
TEST_FUNCS+=(clang_analyzer_test)

shellcheck_test()
{
	log "Run shellcheck"
	shellcheck scripts/*.sh 1>&2
}
TEST_FUNCS+=(shellcheck_test)

test_exists()
{
	local tst=$1
	local func
	for func in "${TEST_FUNCS[@]}"; do
		if [ "${tst}_test" = "$func" ]; then
			return 0
		fi
	done
	return 1
}

if [[ $# == 0 ]]; then
	for func in "${TEST_FUNCS[@]}"; do
		eval "$func"
	done
else
	for tst; do
		if ! test_exists "$tst"; then
			echo 1>&2 "Unknown test: $tst"
			exit 2
		fi
	done
	for tst; do
		eval "${tst}_test"
	done
fi

log "All tests passed!"
