Replace run_tests.sh with a dynamically-generated Makefile

The script's main purpose is to spawn a parallel job per test case
and configuration. Since that is what Make does for compilation jobs,
this patch turns test jobs into Make targets.

Replace run_tests.sh with gen_makefile.sh, which enumerates tests in
the kvm-unit-tests output folder and generates one Make target per
test case and configuration. Rebuild the list whenever kvm-unit-tests
are recompiled.

Example usage:
	$ make test -j20            # run all tests, 20 parallel jobs
	$ make test-kut-nvhe        # run all nVHE tests
	$ make test-kut-nvhe-psci   # run one specific test

Change-Id: Ic1930c752814e68f8437ceb08e9b8c975ceca047
diff --git a/Makefile b/Makefile
index ee8665a..8c71dd1 100644
--- a/Makefile
+++ b/Makefile
@@ -12,6 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+SELF := $(abspath $(lastword $(MAKEFILE_LIST)))
+
 ##
 ## Folders
 ##
@@ -19,7 +21,7 @@
 OUT_DIR := $(ROOT_DIR)/out
 LINUX_OUT := $(OUT_DIR)/linux
 CCACHE_DIR := $(OUT_DIR)/.ccache
-TEST_SCRIPTS_DIR := $(ROOT_DIR)/build/aarch64-unit-tests
+SCRIPTS_DIR := $(ROOT_DIR)/build
 
 ##
 ## Common options
@@ -76,8 +78,12 @@
 	@echo '   kvm-unit-tests_clean - the kvm-unit-tests'
 	@echo '   linux_clean          - the linux kernel'
 	@echo ''
+	@echo 'Test Targets:'
+	@echo '   test                 - runs all tests in qemu-aarch64 simulated environment'
+	@echo '   test-kut             - runs all kvm-unit-tests in qemu-aarch64 simulated environment'
+	@echo '   test-list            - lists all test targets'
+	@echo ''
 	@echo 'Misc Targets:'
-	@echo '   test                 - runs the kvm unit tests in the qemu-aarch64 simulated environment'
 	@echo '   update-prebuilts     - generates/update the prebuilt rootfs image (aarch64) and the qemu host (aarch64)'
 
 
@@ -110,6 +116,7 @@
 KUT_ARCH := "arm64"
 KUT_SRC := $(ROOT_DIR)/kvm-unit-tests
 KUT_OUT := $(OUT_DIR)/kvm-unit-tests
+KUT_STAMP := $(KUT_OUT)/kvm-unit-tests.stamp
 KUT_CC := $(TOOLCHAIN_CLANG)/bin/clang
 KUT_LD := $(TOOLCHAIN_CLANG)/bin/ld.lld
 KUT_OBJCOPY := $(TOOLCHAIN_CLANG)/bin/llvm-objcopy
@@ -119,7 +126,7 @@
 
 
 .PHONY: kvm-unit-tests
-kvm-unit-tests:
+kvm-unit-tests $(KUT_STAMP):
 	mkdir -p $(KUT_OUT)
 	cd $(KUT_OUT) && \
 		$(KUT_SRC)/configure \
@@ -129,9 +136,11 @@
 			--objcopy=$(KUT_OBJCOPY) --objdump=$(KUT_OBJDUMP)
 	PATH=$(KUT_PATH) COMMON_CFLAGS=$(KUT_COMMON_CFLAGS) \
 		$(MAKE) -C $(KUT_OUT) standalone
+	touch $(KUT_STAMP)
 
 .PHONY: kvm-unit-tests_clean
 kvm-unit-tests_clean:
+	- rm -f $(KUT_STAMP)
 	- $(MAKE) -C $(KUT_OUT) clean
 
 
@@ -156,18 +165,23 @@
 	V=$(LINUX_VERBOSE) \
 	O=$(LINUX_OUT)
 
-.PHONY: linux
-linux:
+LINUX_CONFIG := $(LINUX_OUT)/.config
+
+.PHONY: linux_defconfig
+linux_defconfig $(LINUX_CONFIG):
 	+ $(LINUX_MAKE) $(LINUX_DEFCONFIG)
+
+.PHONY: linux
+linux: $(LINUX_CONFIG)
 	+ $(LINUX_MAKE)
 
 .PHONY: linux_image
-linux_image:
+linux_image: $(LINUX_CONFIG)
+	+ $(LINUX_MAKE) Image.gz
+
 # If using own kernel image (KERNEL_IMAGE is set in the environment), then skip.
 ifeq ($(CUSTOM_KERNEL_IMAGE), 0)
-	echo $(CUSTOM_KERNEL_IMAGE)
-	+ $(LINUX_MAKE) $(LINUX_DEFCONFIG)
-	+ $(LINUX_MAKE) Image.gz
+$(KERNEL_IMAGE): linux_image
 endif
 
 .PHONY: linux_clean
@@ -200,29 +214,35 @@
 
 
 ##
-## Run unit tests
+## Run tests
 ##
 
-MAKE_PID := $(shell echo $$PPID)
-JOB_FLAG := $(filter -j%, $(subst -j ,-j,$(shell ps T | grep "^\s*$(MAKE_PID).*$(MAKE)")))
-JOBS     := $(subst -j,,$(JOB_FLAG))
-ifeq ($(JOBS),)
-	JOBS := "1"
-endif
+# Root test targets. Dynamically generated per-test targets
+# will add themselves as dependencies of these.
+.PHONY: test test-list
+test:
+test-list:
 
-KUT_QEMU_BIN := $(ROOT_DIR)/prebuilts/linux-x86/qemu/bin/qemu-system-aarch64.sh
-KUT_ROOTFS_IMAGE := $(ROOT_DIR)/prebuilts/linux-aarch64/images/rootfs.ext4
+#
+# kvm-unit-tests
+#
+
+KUT_GEN_MAKEFILE := $(SCRIPTS_DIR)/kvm-unit-tests/gen_makefile.sh
+KUT_MAKEFILE := $(OUT_DIR)/Makefile.kvm-unit-tests
+KUT_TEST_DIR := $(KUT_OUT)/tests
+KUT_LOG_DIR := $(OUT_DIR)/test/kvm-unit-tests/
 
 # Exclude tests that require user interaction or are known to fail.
-KUT_EXCLUDE := "(.+migrat.+)|(gicv2-.+)|(pmu-event-introspection)|(micro-bench)"
+KUT_EXCLUDE := (.+migrat.+)|(gicv2-.+)|(pmu-event-introspection)|(micro-bench)
 
-.PHONY: test
-test: kvm-unit-tests linux_image
-	@VERBOSE=$(VERBOSE) \
-		$(TEST_SCRIPTS_DIR)/run_tests.sh \
-			-j $(JOBS) \
-			-t $(KUT_OUT)/tests/ \
-			-x $(KUT_EXCLUDE) \
-			-l $(KERNEL_IMAGE) \
-			-r $(KUT_ROOTFS_IMAGE) \
-			-e $(KUT_QEMU_BIN)
+# Generate a Makefile with targets per test and configuration.
+# Pass in variable/target names from this Makefile that the
+# generated Makefile will refer to.
+$(KUT_MAKEFILE): $(SELF) $(KUT_GEN_MAKEFILE) $(KUT_STAMP)
+	@ mkdir -p $(shell dirname $@)
+	@ $(KUT_GEN_MAKEFILE)				\
+		$(KUT_TEST_DIR) "$(KUT_EXCLUDE)"	\
+		KERNEL_IMAGE KUT_LOG_DIR test test-list	\
+		> $@.tmp
+	@ mv $@.tmp $@
+include $(KUT_MAKEFILE)
diff --git a/aarch64-unit-tests/run_tests.sh b/aarch64-unit-tests/run_tests.sh
deleted file mode 100755
index c292d98..0000000
--- a/aarch64-unit-tests/run_tests.sh
+++ /dev/null
@@ -1,181 +0,0 @@
-#!/usr/bin/env bash
-
-# Copyright 2020 The Android KVM Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-exit_status=0
-script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
-test_dir=""
-test_script="${script_dir}/../kvm-unit-tests/run_test.sh"
-log_dir=${LOG_DIR:-out/test/kvm-unit-tests}
-exclude_regex=""
-j=1
-linux=""
-rootfs=""
-verbose=${VERBOSE:-0}
-qemu=${QEMU:-qemu-system-aarch64}
-
-function get_outcome()
-{
-    grep -E -h --color=never "[^ ](PASS|FAIL|SKIP)[^:]"
-}
-
-function usage()
-{
-cat <<EOF
-
-Usage: $0 [-h] [-t TEST-FILES] [-j NUM-TASKS] [-l KERNEL] [-e QEMU] [-r ROOT]
-
-    -h    Output this help text
-    -t    kvm-unit-tests standalone directory
-    -x    tests to exclude (regex), can be specified more than once
-    -j    Execute tests in parallel
-    -l    Linux kernel image to use
-    -e    QEMU emulator binary
-    -r    Root file system image to use
-
-EOF
-}
-
-function usage_abort()
-{
-    usage
-    exit 1
-}
-
-function wait_free_job_slots()
-{
-	while (( $(jobs | wc -l) == j )); do
-		# Wait for enough jobs to finish
-		wait -n || exit_status=1
-	done
-}
-
-function wait_background_jobs()
-{
-	while (( $(jobs | wc -l) > 0 )); do
-		# Wait for background jobs
-		wait -n || exit_status=1
-	done
-}
-
-function run_job()
-{
-        wait_free_job_slots
-
-	if [ "$j" = 1 ]; then
-		bash -c "$@" || exit_status=1
-	else
-		bash -c "$@" &
-	fi
-}
-
-while getopts ":t:x:j:l:r:e:hv" opt; do
-    case "${opt}" in
-        t)
-            test_dir=${OPTARG}
-            ;;
-        x)
-            # OR with the previous regex if specified
-            if [ -n "${exclude_regex}" ]; then
-              exclude_regex+="|"
-            fi
-            exclude_regex+="(^${OPTARG}$)"
-            ;;
-        j)
-            j=${OPTARG}
-            ;;
-        l)
-            linux=${OPTARG}
-            ;;
-        r)
-            rootfs=${OPTARG}
-            ;;
-        e)
-            qemu=${OPTARG}
-            ;;
-        h)
-            usage
-            exit 0
-            ;;
-        v)
-            verbose=1
-            ;;
-        *)
-            usage_abort
-            ;;
-    esac
-done
-if ((OPTIND == 1)); then
-    usage_abort
-fi
-shift $((OPTIND-1))
-
-if [[ ! -d ${test_dir} ]]; then
-    echo "No test directory specified."
-    usage_abort
-fi
-
-if [[ ! -f ${linux} ]]; then
-	echo "Linux kernel image file not found."
-	usage_abort
-fi
-
-if [[ ! -f ${rootfs} ]]; then
-	echo "Root filesystem image file not found."
-	usage_abort
-fi
-
-if [[ ! -x ${qemu} ]]; then
-	echo "QEMU executable not found."
-	usage_abort
-fi
-
-rm -rf "$log_dir"/logs.old
-if [ -d "$log_dir"/logs ]; then
-    mv "$log_dir"/logs "$log_dir"/logs.old
-fi
-mkdir -p "$log_dir"/logs/vhe || exit 2
-mkdir -p "$log_dir"/logs/nvhe || exit 2
-
-for test in "${test_dir}"/*; do
-    test_name=$(basename "${test}")
-
-    if [[ -n "${exclude_regex}" && "${test_name}" =~ ${exclude_regex} ]]; then
-        if [ "$verbose" != "0" ]; then
-            echo "Excluding ${test_name}"
-        fi
-        continue
-    fi
-
-    if [ "$verbose" != "0" ]; then
-        echo "Running ${test_name}"
-    fi
-
-    # Redirect output instead of pipe to preserve exit status.
-    run_job "${test_script} -V -k ${linux}                          \
-            -o ${log_dir}/logs/vhe/${test_name}.log                 \
-            -d \"${test_name} (vhe)\" ${test}                       \
-            -- -e ${qemu} -r ${rootfs}                              \
-            &> >(tail -1)"
-    run_job "${test_script} -k ${linux}                             \
-            -o ${log_dir}/logs/nvhe/${test_name}.log                \
-            -d \"${test_name} (nvhe)\" ${test}                       \
-            -- -e ${qemu} -r ${rootfs}                              \
-            &> >(tail -1)"
-done
-
-wait_background_jobs
-
-exit $exit_status
diff --git a/common.inc b/common.inc
index 29dfaa1..c2443bb 100644
--- a/common.inc
+++ b/common.inc
@@ -20,6 +20,7 @@
 ROOT_DIR="$(dirname "${SCRIPT_DIR}")"
 
 SCRIPT_RUN_QEMU="${SCRIPT_DIR}/aarch64/run_qemu.sh"
+SCRIPT_RUN_KUT="${SCRIPT_DIR}/kvm-unit-tests/run_test.sh"
 
 PREBUILTS_DIR="${ROOT_DIR}/prebuilts"
 PREBUILTS_IMG_DIR="${PREBUILTS_DIR}/linux-aarch64/images"
diff --git a/kvm-unit-tests/gen_makefile.sh b/kvm-unit-tests/gen_makefile.sh
new file mode 100755
index 0000000..aafc2db
--- /dev/null
+++ b/kvm-unit-tests/gen_makefile.sh
@@ -0,0 +1,153 @@
+#!/usr/bin/env bash
+
+# Copyright 2020 The Android KVM Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+source "$(dirname "${BASH_SOURCE[0]}")/../common.inc"
+
+# Directory containing kvm-unit-tests' binaries
+TEST_DIR="$1"
+
+# Regex for excluding known-to-fail or irrelevant tests.
+EXCLUDE="$2"
+
+# Variable/target names from the root Makefile.
+VAR_KERNEL_IMAGE="$3"
+VAR_LOG_DIR="$4"
+GLOBAL_TEST_TARGET="$5"
+GLOBAL_TEST_LIST_TARGET="$6"
+
+# Generates a Make target name for a test case.
+# Args:
+#   1) name of test case
+#   2) target name of the test's group.
+function target_name {
+	local target="$1"
+	local parent="$2"
+	echo "${parent}-${target}"
+}
+
+# Generates target name for a group of given parameters.
+# Args:
+#   1) Root target name
+#   2) is VHE (0/1)
+function group_target_name {
+	local group="$1"
+	local vhe="$2"
+	group="$(target_name kut "${group}")"
+	if [ "${vhe}" -eq 1 ]; then
+		group="$(target_name vhe "${group}")"
+	else
+		group="$(target_name nvhe "${group}")"
+	fi
+	echo "${group}"
+}
+
+# Generates target name for a target printing list of all test targets.
+# Args:
+#   1) Root target name
+function list_target_name {
+	local name="$1"
+	name="$(target_name kut "${name}")"
+	echo "${name}"
+}
+
+# Emits a Make target and a dependency of its parent on the target.
+# Args:
+#   1) Target name
+#   2) Parent target name
+function dependency_target {
+	local target="$1"
+	local parent="$2"
+	cat <<EOF
+.PHONY: ${target}
+${target}:
+${parent}: ${target}
+EOF
+}
+
+# Emits a Make target for a test case.
+# Args:
+#   1) Target name
+#   2) Test binary path
+#   3) is VHE (0/1)
+function test_target {
+	local target="$1"
+	local binary="$2"
+	local vhe="$3"
+
+	local extra_args=()
+	if [ "${vhe}" -eq 1 ]; then
+		extra_args+=(-V)
+	fi
+
+	cat <<EOF
+.PHONY: ${target}
+${target}: \$(${VAR_KERNEL_IMAGE}) ${binary}
+	@ mkdir -p "\$(${VAR_LOG_DIR})"
+	@ "${SCRIPT_RUN_KUT}"				\
+		-k \$(${VAR_KERNEL_IMAGE})		\
+		-d "${target}"				\
+		-o \$(${VAR_LOG_DIR})/${target}.log	\
+		${extra_args[@]}			\
+		"${binary}"
+EOF
+}
+
+# Emits a Make target which prints all provided target names,
+# one per line.
+# Args:
+#   1)   target name
+#   ...) list of target names to print
+function test_list_target {
+	local target="$1"
+	shift 1
+	cat <<EOF
+.PHONY: ${target}
+${target}:
+	@ for T in $@; do echo "\$\$T"; done
+EOF
+}
+
+if [ ! -d "${TEST_DIR}" ]; then
+	echo "ERROR: Test directory does not exist" 1>&2
+	exit 1
+fi
+
+TARGET_LIST=()
+for VHE in 0 1; do
+	# Emit a target for this test group.
+	ROOT="${GLOBAL_TEST_TARGET}"
+	GROUP="$(group_target_name "${ROOT}" "${VHE}")"
+	dependency_target "${GROUP}" "${ROOT}"
+
+	for TEST_PATH in "${TEST_DIR}"/*; do
+		NAME=$(basename "${TEST_PATH}")
+		if [[ ${NAME} =~ ${EXCLUDE} ]]; then
+			continue
+		fi
+
+		# Emit a target for this test case.
+		TARGET="$(target_name ${NAME} ${GROUP})"
+		test_target "${TARGET}" "${TEST_PATH}" "${VHE}"
+		dependency_target "${TARGET}" "${GROUP}"
+		TARGET_LIST+=("${TARGET}")
+	done
+done
+
+# Emit a target which prints all test targets from the generated Makefile.
+ROOT="${GLOBAL_TEST_LIST_TARGET}"
+TARGET="$(list_target_name "${ROOT}")"
+test_list_target "${TARGET}" "${TARGET_LIST[@]}"
+dependency_target "${TARGET}" "${ROOT}"