diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c8acca4..175b986d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,7 +77,7 @@ jobs: - name: Unit Test run: make test - # this job is for compilation test, it will make sure all aaarch64, riscv64, loongarch64 can be compiled successfully + # this job is for compilation test, it will make sure all aarch64, riscv64, loongarch64 can be compiled successfully # there is no actual running in this job build: name: build @@ -143,6 +143,45 @@ jobs: run: make # this job is for booting root and nonroot inside qemu for system test - # curently waiting for another person to add his system test here - # systemtest: - # ... + systemtest: + name: systemtest + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + # aarch64 + - arch: aarch64 + rustc_target: aarch64-unknown-none + features: "platform_qemu,gicv3" + board: "qemu" + # currently supports only "platform_qemu,gicv3" + # Because other features need to be customized scripts + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + - name: Install Rust Toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.rustc_target }} + components: rust-src + - name: Install Dependencies + run: | + sudo apt-get update + sudo apt-get install -y qemu-system-aarch64 qemu-system-riscv64 gdb-multiarch llvm-dev libclang-dev wget expect device-tree-compiler p7zip-full + cargo install --version 0.3.0 cargo-binutils + cargo install cargo-xbuild + - name: Set up environment variables + run: | + echo "ARCH=${{ matrix.arch }}" >> $GITHUB_ENV + echo "FEATURES=${{ matrix.features }}" >> $GITHUB_ENV + echo "BOARD=${{ matrix.board }}" >> $GITHUB_ENV + - name: Compile DTB + run: | + ./test/tcompiledtb.sh + - name: Download rootfs and image + run: | + ./test/tdownload_all.sh + - name: Run Tests + run: | + ./test/tstart.sh diff --git a/scripts/qemu-aarch64.mk b/scripts/qemu-aarch64.mk index c9a2ff22..5e5809f0 100644 --- a/scripts/qemu-aarch64.mk +++ b/scripts/qemu-aarch64.mk @@ -39,4 +39,15 @@ $(hvisor_bin): elf $(OBJCOPY) $(hvisor_elf) --strip-all -O binary $(hvisor_bin).tmp && \ mkimage -n hvisor_img -A arm64 -O linux -C none -T kernel -a 0x40400000 \ -e 0x40400000 -d $(hvisor_bin).tmp $(hvisor_bin) && \ - rm -rf $(hvisor_bin).tmp \ No newline at end of file + rm -rf $(hvisor_bin).tmp + +QEMU_ARGS += -netdev type=user,id=net1 +QEMU_ARGS += -device virtio-net-pci,netdev=net1,disable-legacy=on,disable-modern=off,iommu_platform=on + +# QEMU_ARGS += -device pci-testdev + +QEMU_ARGS += -netdev type=user,id=net2 +QEMU_ARGS += -device virtio-net-pci,netdev=net2,disable-legacy=on,disable-modern=off,iommu_platform=on + +QEMU_ARGS += -netdev type=user,id=net3 +QEMU_ARGS += -device virtio-net-pci,netdev=net3,disable-legacy=on,disable-modern=off,iommu_platform=on \ No newline at end of file diff --git a/test/tcompiledtb.sh b/test/tcompiledtb.sh new file mode 100755 index 00000000..df53fb73 --- /dev/null +++ b/test/tcompiledtb.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e # Exit immediately if any command fails + +# Compile device tree in a subshell to maintain working directory +( + cd images/aarch64/devicetree && + make all +) +# Subshell automatically returns to original directory after execution \ No newline at end of file diff --git a/test/tdownload_all.sh b/test/tdownload_all.sh new file mode 100755 index 00000000..396e26aa --- /dev/null +++ b/test/tdownload_all.sh @@ -0,0 +1,160 @@ +#!/bin/bash + +# Split archive + independent image download/merge/unzip script +# Usage: ./download_all.sh + +# Configuration parameters +RELEASE_NAME="v2025.03.01" +BASE_URL="https://github.com/CHonghaohao/hvisor_env_img/releases/download/$RELEASE_NAME" + +# Split archive configuration (must be in order) +ZIP_PARTS=( + "rootfs1.zip.001" + "rootfs1.zip.002" + "rootfs1.zip.003" +) +ZIP_OUTPUT="rootfs1.zip" +UNZIP_DIR="images/aarch64/virtdisk" # Extraction directory + +# Independent image configuration +TARGET_DIR="images/aarch64/kernel" # Target directory path +IMAGE_FILE="${TARGET_DIR}/Image" # Full image file path +IMAGE_URL="$BASE_URL/Image" + +# Download control parameters +MAX_RETRIES=3 # Max retries per file +PARALLEL_DOWNLOADS=1 # Parallel downloads (improves speed for large files) +TIMEOUT=3600 # Timeout per file (seconds) + +# Color definitions +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' + +# Check dependencies +check_dependencies() { + local missing=() + command -v unzip >/dev/null 2>&1 || missing+=("unzip") + command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1 || missing+=("curl/wget") + + if [ ${#missing[@]} -gt 0 ]; then + echo -e "${RED}Error: Missing dependencies - ${missing[*]}${NC}" + exit 1 + fi +} + +# Download function with progress display +download_file() { + local url="$1" + local output="$2" + local retries=0 + + while [ $retries -lt $MAX_RETRIES ]; do + if [ -f "$output" ]; then + local current_size=$(stat -c%s "$output" 2>/dev/null || echo 0) + if command -v curl >/dev/null 2>&1; then + curl -C - -# -L --retry 2 --max-time $TIMEOUT -o "$output" "$url" && return 0 + elif command -v wget >/dev/null 2>&1; then + wget -c -q --show-progress --tries=2 --timeout=$TIMEOUT -O "$output" "$url" && return 0 + fi + else + if command -v curl >/dev/null 2>&1; then + curl -# -L --retry 2 --max-time $TIMEOUT -o "$output" "$url" && return 0 + elif command -v wget >/dev/null 2>&1; then + wget -q --show-progress --tries=2 --timeout=$TIMEOUT -O "$output" "$url" && return 0 + fi + fi + + ((retries++)) + echo -e "${YELLOW}Retry ($retries/$MAX_RETRIES): $output${NC}" + sleep 2 + done + + echo -e "${RED}Download failed: $url${NC}" + return 1 +} + +# Main process +main() { + check_dependencies + + # Check if final files exist + if [ -d "$UNZIP_DIR" ] && [ -f "$IMAGE_FILE" ]; then + echo -e "${GREEN}All files already exist:\n- Image file: $IMAGE_FILE\n- Extracted directory: $UNZIP_DIR${NC}" + exit 0 + fi + + # Parallel download split files + echo -e "${YELLOW}Starting split file downloads (parallel: $PARALLEL_DOWNLOADS)...${NC}" + for part in "${ZIP_PARTS[@]}"; do + local url="$BASE_URL/$part" + local output="$part" + + if [ -f "$output" ]; then + echo -e "${GREEN}Part already exists: $output${NC}" + continue + fi + + ((i=i%PARALLEL_DOWNLOADS)); ((i++==0)) && wait + ( + if download_file "$url" "$output"; then + echo -e "${GREEN}Download completed: $output${NC}" + else + exit 1 + fi + ) & + done + wait + + # Verify split file integrity + for part in "${ZIP_PARTS[@]}"; do + if [ ! -f "$part" ]; then + echo -e "${RED}Missing part: $part${NC}" + exit 1 + fi + done + + # Merge split files + if [ ! -f "$ZIP_OUTPUT" ]; then + echo -e "${YELLOW}Merging split files -> $ZIP_OUTPUT ...${NC}" + cat "${ZIP_PARTS[@]}" > "$ZIP_OUTPUT" || { + echo -e "${RED}Merge failed!${NC}" + exit 1 + } + else + echo -e "${GREEN}Using existing merged file: $ZIP_OUTPUT${NC}" + fi + + # Unzip files + if [ ! -d "$UNZIP_DIR" ]; then + echo -e "${YELLOW}Extracting to directory: $UNZIP_DIR ...${NC}" + unzip -q "$ZIP_OUTPUT" -d "$UNZIP_DIR" || { + echo -e "${RED}Extraction failed! Possible reasons:\n1. Password protected\n2. Corrupted file${NC}" + exit 1 + } + fi + + # Download independent image + echo -e "${YELLOW}Downloading image file: $IMAGE_FILE ...${NC}" + mkdir -p "$TARGET_DIR" || { + echo -e "${RED}Failed to create directory: $TARGET_DIR${NC}" + exit 1 + } + + if [ -f "$IMAGE_FILE" ]; then + echo -e "${GREEN}Image already exists: $IMAGE_FILE${NC}" + else + download_file "$IMAGE_URL" "$IMAGE_FILE" || { + echo -e "${RED}Download failed: $IMAGE_FILE${NC}" + exit 1 + } + fi + + # Final verification + echo -e "\n${GREEN}All components ready: " + echo -e " - Image file: $(ls -lh $IMAGE_FILE)" + echo -e " - Extracted directory: $(du -sh $UNZIP_DIR)${NC}" +} + +main diff --git a/test/testcase/tc_insmod.txt b/test/testcase/tc_insmod.txt new file mode 100644 index 00000000..f315143f --- /dev/null +++ b/test/testcase/tc_insmod.txt @@ -0,0 +1,2 @@ +hvisor: loading out-of-tree module taints kernel. +hvisor init done!!! diff --git a/test/testcase/tc_ls.txt b/test/testcase/tc_ls.txt new file mode 100644 index 00000000..b1b0e34f --- /dev/null +++ b/test/testcase/tc_ls.txt @@ -0,0 +1,35 @@ +1.c +2 +2.c +Image +a.out +byte-unixbench-5.1.3 +hvisor +hvisor.ko +linux2.dtb +linux2.json +linux2.sh +linux2_back.dtb +linux3.dtb +linux3.json +linux3.sh +lmbench-3.0-a9 +log.txt +ltp +main.ko +msi_engine.ko +nohup.out +pci +pciutils-3.13.0 +rootfs2.ext4 +screen_linux2.sh +setup.sh +sysbench-1.0.20 +test +test.zip +test_nohup1.txt +virtio.json +virtio_cfg.json +virtio_cfg2.json +virtio_cfg3.json +å diff --git a/test/testcase/tc_pwd.txt b/test/testcase/tc_pwd.txt new file mode 100644 index 00000000..27243b72 --- /dev/null +++ b/test/testcase/tc_pwd.txt @@ -0,0 +1 @@ +/home/arm64 diff --git a/test/testcase/tc_zone1_ls.txt b/test/testcase/tc_zone1_ls.txt new file mode 100644 index 00000000..0309ecc1 --- /dev/null +++ b/test/testcase/tc_zone1_ls.txt @@ -0,0 +1 @@ +zone1.txt diff --git a/test/testcase/tc_zone1_start.txt b/test/testcase/tc_zone1_start.txt new file mode 100644 index 00000000..09afb260 --- /dev/null +++ b/test/testcase/tc_zone1_start.txt @@ -0,0 +1,2 @@ +non root region mmap succeed! +hvisor: calling hypercall to start zone diff --git a/test/testcase/tc_zone_list1.txt b/test/testcase/tc_zone_list1.txt new file mode 100644 index 00000000..c59ddd83 --- /dev/null +++ b/test/testcase/tc_zone_list1.txt @@ -0,0 +1,2 @@ +| zone_id | cpus | name | +| 0 | 0, 1 | root-linux | diff --git a/test/testcase/tc_zone_list2.txt b/test/testcase/tc_zone_list2.txt new file mode 100644 index 00000000..0c539983 --- /dev/null +++ b/test/testcase/tc_zone_list2.txt @@ -0,0 +1,3 @@ +| zone_id | cpus | name | +| 0 | 0, 1 | root-linux | +| 1 | 2, 3 | linux2 | diff --git a/test/textract_dmesg.sh b/test/textract_dmesg.sh new file mode 100755 index 00000000..a6ae42f1 --- /dev/null +++ b/test/textract_dmesg.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +# Define a function to process dmesg output +extract_dmesg() { + local output_file="$1" # The first parameter is output file path + + # Capture dmesg output + local dmesg_output=$(dmesg) + + # Process output with awk + echo "$dmesg_output" | awk ' + BEGIN { + RS="\n"; # Set record separator to newline + } + { + # Remove leading [] timestamps + sub(/^\[[^]]*\] /, "") + # Store processed lines in array + lines[NR] = $0 + } + END { + # Initialize counters and output arrays + count = 0 + output_lines[1] = "" + output_lines[2] = "" + # Traverse from last line backwards + for (i = NR; i > 0; i--) { + if (lines[i] !~ /random: fast init done/) { + # If line does not contain - random: fast init done - + if (count < 2) { + # Store line in output array + output_lines[2-count] = lines[i] + count++ + } + } + # Break loop when count reaches 2 + if (count >= 2) { + break + } + } + # Output lines in correct order + if (output_lines[1] != "") { + printf "%s\n", output_lines[1] + } + if (output_lines[2] != "") { + printf "%s\n", output_lines[2] + } + } + ' > "$output_file" +} + +# Call function with output file path +extract_dmesg "$1" \ No newline at end of file diff --git a/test/tresult.sh b/test/tresult.sh new file mode 100755 index 00000000..db51cff6 --- /dev/null +++ b/test/tresult.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +# set -x +# Define two arrays to store filenames from two lists +testcase_file_list=( + ./test/testcase/tc_ls.txt + ./test/testcase/tc_pwd.txt + ./test/testcase/tc_insmod.txt + # ./test/testcase/tc_zone1_start.txt + ./test/testcase/tc_zone1_ls.txt + ./test/testcase/tc_zone_list2.txt + ./test/testcase/tc_zone_list1.txt +) + +testresult_file_list=( + ./test/testresult/test_ls.txt + ./test/testresult/test_pwd.txt + ./test/testresult/test_insmod.txt + # ./test/testresult/test_zone1_start.txt + ./test/testresult/test_zone1_ls.txt + ./test/testresult/test_zone_list2.txt + ./test/testresult/test_zone_list1.txt +) + +testcase_name_list=( + ls_out + pwd_out + insmod_hvisor.ko + # zone1_start_out + zone1_start + zone_list + zone1_shutdown +) + +# Get the length of the file lists +testcase_file_list_len=${#testcase_file_list[@]} +testresult_file_list_len=${#testresult_file_list[@]} + +# Check if the lengths of the two lists are equal +if [ "$testcase_file_list_len" -ne "$testresult_file_list_len" ]; then + echo "Error: The length of the two file lists is not equal." + exit 1 # Return error status code 1 +fi + +fail_count=0 +# Loop through the file lists +for ((i = 0; i < testcase_file_list_len; i++)); do + # Get the ith filename from the lists + testcase_file=${testcase_file_list[i]} + testresult_file=${testresult_file_list[i]} + testcase_name=${testcase_name_list[i]} + + # Send the diff command and wait for it to complete + diff "$testcase_file" "$testresult_file" + exit_status=$? + + # Output the result based on the exit status + if [ "$exit_status" -eq 0 ]; then + echo "$testcase_name $testresult_file PASS" >> ./test/result.txt + else + fail_count=$((fail_count+1)) # Increment fail_count + echo "$testcase_name $testresult_file FAIL" >> ./test/result.txt + fi +done + + +cat ./test/result.txt +# Format the output file content +printf "\n%-17s | %-40s | %s\n" "test name" "test result file" "result" +# Read the file content +while IFS= read -r line; do + # Use regex to extract the test case name and result + if [[ $line =~ ([^[:space:]]+)\ +(.*)\ +([A-Z]+)$ ]]; then + testname=${BASH_REMATCH[1]} + testcase=${BASH_REMATCH[2]} + result=${BASH_REMATCH[3]} + + # Format the output + printf "%-17s | %-40s | %s\n" "$testname" "$testcase" "$result" + fi +done < "./test/result.txt" +printf "\n" + +# Delete the generated files +rm -v ./test/testresult/test_*.txt +rm -v ./test/result.txt + +# Check if failcount is greater than 0 +if [ "$fail_count" -gt 0 ]; then + echo "Error: Test fail. Exiting script." + exit 1 # Exit with error, return status code 1 +else + echo "All tests passed. Script is exiting normally." + exit 0 # Exit normally, return status code 0 +fi \ No newline at end of file diff --git a/test/tstart.sh b/test/tstart.sh new file mode 100755 index 00000000..33f5c493 --- /dev/null +++ b/test/tstart.sh @@ -0,0 +1,220 @@ +#!/usr/bin/expect -f + +# Set environment variables to support UTF-8 +set env(LANG) "en_US.UTF-8" +send_user "\r============Starting automated script execution============\r" +spawn make run + +# Set timeout (adjust as needed) +set timeout 240 +# set password [lindex $argv 0] + +# Wait for root password prompt and U-Boot prompt +expect { +# "password for chh: " { +# puts "\r============Handling sudo password and U-Boot commands============\r" +# send "$password\r" +# exp_continue +# } + -re "(1 bootflow, 1 valid).*=>" { + # Enter command at prompt + send "bootm 0x40400000 - 0x40000000\r" + } + timeout { + exit 1 + } +} + +puts "\n============Testing hvisor startup and virtio daemon============\n" + +expect { + -re {job control turned off.*#} { + send "bash\r" + } + timeout { + exit 1 + } +} + +expect { + "root@(none):/# " { + send "cd /home/arm64\r" + } + timeout { + exit 1 + } +} + +# Test ls command +expect { + "root@(none):/home/arm64# " { + send "ls > ./test/testresult/test_ls.txt\r" + } + timeout { + exit 1 + } +} + +# Test pwd command +expect { + "root@(none):/home/arm64# " { + send "pwd > ./test/testresult/test_pwd.txt\r" + } + timeout { + exit 1 + } +} + +# Test kernel module loading +expect { + "root@(none):/home/arm64# " { + send "insmod hvisor.ko\r" + } + timeout { + exit 1 + } +} +expect { + "root@(none):/home/arm64# " { + # send "dmesg | tail -n 2 | awk -F ']' '{print \$2}' > ./test/testresult/test_insmod.txt\r" + send "./test/textract_dmesg.sh ./test/testresult/test_insmod.txt\r" + } + timeout { + exit 1 + } +} + + +# Test starting zone1 +expect { + "root@(none):/home/arm64# " { + send "./linux2.sh\r" + } + timeout { + exit 1 + } +} +expect { + -re {Script started, file is /dev/null.*#} { + send "bash\r" + } + timeout { + exit 1 + } +} +# Temporarily skip checking zone1 startup based on Log +expect { + "root@(none):/home/arm64# " { + send "dmesg | tail -n 3 | awk -F ']' '{print \$2}' > ./test/testresult/test_zone1_start.txt\r" + send "./test/textract_dmesg.sh ./test/testresult/test_zone1_start.txt\r" + } + timeout { + exit 1 + } +} + +# Test screen access to zone1 +expect { + "root@(none):/home/arm64# " { + send "./screen_linux2.sh\r" + send "\r" + } + timeout { + exit 1 + } +} +expect { + "# " { + send "bash\r" + } + timeout { + exit 1 + } +} +# Variable to store zone1 ls command output +set test_zone1_ls "" +expect "root@(none):/# " +send "cd /home/arm64\r" +expect "root@(none):/home/arm64# " +# Send ls command and capture output to determine if zone1 is entered +send "ls | grep zone1.txt\r" +expect { + -re {^[^\n]+\n(.*)\r\r\nroot@\(none\):/home/arm64# } { + set test_zone1_ls $expect_out(1,string) + send "\x01\x01d" + } + timeout { + exit 1 + } +} +expect { + "root@(none):/home/arm64# " { + send "echo \"$test_zone1_ls\" > ./test/testresult/test_zone1_ls.txt\r" + } + timeout { + exit 1 + } +} + +# Test printing zone list after starting zone1 +expect { + "root@(none):/home/arm64# " { + send "./hvisor zone list > ./test/testresult/test_zone_list2.txt\r" + } + timeout { + exit 1 + } +} + +# Shutting down zone1 +expect { + "root@(none):/home/arm64# " { + send "./hvisor zone shutdown -id 1\r" + } + timeout { + exit 1 + } +} + +# Test printing zone list after removing zone1 +expect { + "root@(none):/home/arm64# " { + send "./hvisor zone list > ./test/testresult/test_zone_list1.txt\r" + } + timeout { + exit 1 + } +} + +# expect { +# "root@(none):/home/arm64# " { +# send "echo \"Test out finish!!\"\r" +# } +# timeout { +# exit 1 +# } +# } + +after 5000 # Delay 5 seconds +# Compare test results and print finally +expect { + "root@(none):/home/arm64# " { + send "./test/tresult.sh\r" + } + timeout { + exit 1 + } +} + +expect { + "Error: Test fail. Exiting script." { + exit 1 + } + "All tests passed. Script is exiting normally." { + exit 0 + } +} + +# exit +expect eof +exit 0