diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 88e022ad..2cdc96d6 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -84,3 +84,20 @@ jobs: run: | cmake -B build cmake --build build --parallel 2 + test: + runs-on: ubuntu-latest + steps: + - name: Clone repository + uses: actions/checkout@v4 + + - name: Install test dependencies + run: sudo apt-get install -y nasm qemu-system-x86 mtools + + - name: Run tests + run: | + cmake -B build -DEMULATOR_OUTPUT_TYPE=OUTPUT_LOG + cmake --build build --parallel 2 --target filesystem cdrom_test.iso qemu-test + + - name: Check + run: | + cat build/test.log | scripts/tapview || cat build/test.log build/serial.log \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 40b8b2de..a673f673 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -264,4 +264,28 @@ add_custom_target( qemu-grub COMMAND ${EMULATOR} ${EMULATOR_FLAGS} -boot d -cdrom ${CMAKE_BINARY_DIR}/cdrom.iso DEPENDS cdrom.iso -) \ No newline at end of file +) + +# ============================================================================= +# Booting with QEMU+GRUB for testing +# ============================================================================= + +# First, we need to build the ISO for the cdrom. It has a slightly different +# kernel command line including 'test'. +add_custom_target( + cdrom_test.iso + COMMAND cp -rf ${CMAKE_SOURCE_DIR}/iso . + COMMAND mv ${CMAKE_BINARY_DIR}/iso/boot/grub/grub.cfg.runtests ${CMAKE_BINARY_DIR}/iso/boot/grub/grub.cfg + COMMAND cp ${CMAKE_BINARY_DIR}/mentos/bootloader.bin ${CMAKE_BINARY_DIR}/iso/boot + COMMAND grub-mkrescue -o ${CMAKE_BINARY_DIR}/cdrom_test.iso ${CMAKE_BINARY_DIR}/iso + DEPENDS bootloader.bin +) + +# This target runs the emulator, and executes the runtests binary as init process. +# Additionally it passes the '-device isa-debug-exit' option to shutdown qemu +# after the tests are done. +add_custom_target( + qemu-test + COMMAND ${EMULATOR} ${EMULATOR_FLAGS} -serial file:${CMAKE_BINARY_DIR}/test.log -nographic -device isa-debug-exit -boot d -cdrom ${CMAKE_BINARY_DIR}/cdrom_test.iso + DEPENDS cdrom_test.iso +) diff --git a/iso/boot/grub/grub.cfg.runtests b/iso/boot/grub/grub.cfg.runtests new file mode 100644 index 00000000..9cba123b --- /dev/null +++ b/iso/boot/grub/grub.cfg.runtests @@ -0,0 +1,7 @@ +set timeout=0 +set default=0 + +menuentry "MentOS tests" { + multiboot /boot/bootloader.bin runtests + boot +} diff --git a/mentos/src/kernel.c b/mentos/src/kernel.c index f6fa65d3..803e0cb4 100644 --- a/mentos/src/kernel.c +++ b/mentos/src/kernel.c @@ -32,6 +32,7 @@ #include "process/scheduler.h" #include "process/scheduler_feedback.h" #include "stdio.h" +#include "string.h" #include "sys/module.h" #include "sys/msg.h" #include "sys/sem.h" @@ -78,6 +79,9 @@ uintptr_t initial_esp = 0; /// The boot info. boot_info_t boot_info; +/// Flag indicating if we are running tests instead of an interactive session +int runtests = 0; + /// @brief Prints [OK] at the current row and column 60. static inline void print_ok(void) { @@ -403,9 +407,21 @@ int kmain(boot_info_t *boot_informations) print_ok(); //========================================================================== - pr_notice("Creating init process...\n"); - printf("Creating init process..."); - task_struct *init_p = process_create_init("/bin/init"); + // TODO: fix the hardcoded check for the flags set by GRUB + runtests = boot_info.multiboot_header->flags == 0x1a67 && + bitmask_check(boot_info.multiboot_header->flags, MULTIBOOT_FLAG_CMDLINE) && + strcmp((char *)boot_info.multiboot_header->cmdline, "runtests") == 0; + + task_struct *init_p; + if (runtests) { + pr_notice("Creating runtests process...\n"); + printf("Creating runtests process..."); + init_p = process_create_init("/bin/runtests"); + } else { + pr_notice("Creating init process...\n"); + printf("Creating init process..."); + init_p = process_create_init("/bin/init"); + } if (!init_p) { print_fail(); return 1; diff --git a/mentos/src/system/panic.c b/mentos/src/system/panic.c index 2d1e171e..c6c230ca 100644 --- a/mentos/src/system/panic.c +++ b/mentos/src/system/panic.c @@ -5,11 +5,16 @@ #include "system/panic.h" #include "io/debug.h" +#include "io/port_io.h" + +#define SHUTDOWN_PORT 0x604 +extern int runtests; void kernel_panic(const char *msg) { pr_emerg("\nPANIC:\n%s\n\nWelcome to Kernel Debugging Land...\n\n", msg); pr_emerg("\n"); __asm__ __volatile__("cli"); // Disable interrupts + if (runtests) { outports(SHUTDOWN_PORT, 0x2000); } // Terminate qemu running the tests for (;;) __asm__ __volatile__("hlt"); // Decrease power consumption with hlt } diff --git a/programs/CMakeLists.txt b/programs/CMakeLists.txt index cdb15c7b..4449764a 100644 --- a/programs/CMakeLists.txt +++ b/programs/CMakeLists.txt @@ -1,5 +1,6 @@ # List of programs. set(PROGRAM_LIST + runtests.c more.c chmod.c chown.c diff --git a/programs/runtests.c b/programs/runtests.c new file mode 100644 index 00000000..f0f1808d --- /dev/null +++ b/programs/runtests.c @@ -0,0 +1,233 @@ +/// @file runtests.c +/// @brief +/// @copyright (c) 2024 This file is distributed under the MIT License. +/// See LICENSE.md for details. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define SHUTDOWN_PORT 0x604 +/// Second serial port for QEMU. +#define SERIAL_COM2 0x02F8 + +static char *all_tests[] = { + "t_abort", + "t_alarm", + "t_creat", + "t_dup", + "t_exec execl", + "t_exec execlp", + "t_exec execle", + "t_exec execlpe", + "t_exec execv", + "t_exec execvp", + "t_exec execve", + "t_exec execvpe", + "t_fork 10", + "t_gid", + "t_groups", + "t_itimer", + "t_kill", + /* "t_mem", */ + "t_msgget", + /* "t_periodic1", */ + /* "t_periodic2", */ + /* "t_periodic3", */ + "t_schedfb", + "t_semflg", + "t_semget", + "t_semop", + "t_setenv", + "t_shmget", + /* "t_shm_read", */ + /* "t_shm_write", */ + "t_sigaction", + "t_sigfpe", + "t_siginfo", + "t_sigmask", + "t_sigusr", + "t_sleep", + "t_stopcont", + "t_write_read", +}; + +static char **tests = &all_tests[0]; +static int testsc = sizeof(all_tests) / sizeof(all_tests[0]) ; + +static char buf[4096]; +static char* bufpos = buf; + +static int test_out_fd; +static int test_err_fd; + +static int init; + +#define append(...) \ +do { \ + bufpos += sprintf(bufpos, __VA_ARGS__); \ + if (bufpos >= buf + sizeof(buf)) { \ + return -1; \ + } \ +} while(0); + +static int test_out_flush(void) { + int ret = 0; + if (!init) { + ret = printf("%s\n", buf); + } else { + char *s = buf; + while((*s) != 0) + outportb(SERIAL_COM2, *s++); + outportb(SERIAL_COM2, '\n'); + } + bufpos = buf; + *bufpos = 0; + return ret; +} + +static int test_out(const char *restrict format, ...) { + va_list ap; + va_start(ap, format); + bufpos += vsprintf(bufpos, format, ap); + if (bufpos >= buf + sizeof(buf)) { + return -1; + } + va_end(ap); + + return test_out_flush(); +} + +static int test_ok(int test, int success, const char *restrict format, ...) { + if (!success) { + append("not "); + } + append("ok %d - %s", test, tests[test - 1]); + if (format) { + append(": "); + va_list ap; + va_start(ap, format); + bufpos += vsprintf(bufpos, format, ap); + if (bufpos >= buf + sizeof(buf)) { + return -1; + } + va_end(ap); + } + + return test_out_flush(); +} + +static void exec_test(char *test_cmd_line) { + // Setup the childs stdout, stderr streams + close(STDOUT_FILENO); + dup(test_out_fd); + close(STDERR_FILENO); + dup(test_err_fd); + + // Build up the test argv vector + char *test_argv[32]; + char *arg = strtok(test_cmd_line, " "); + test_argv[0] = arg; + + int t_argc = 1; + while ((arg = strtok(NULL, " "))) { + if (t_argc >= sizeof(test_argv) / sizeof(test_argv[0])) { + exit(126); + } + test_argv[t_argc] = arg; + t_argc++; + } + + test_argv[t_argc] = NULL; + + char test_abspath[PATH_MAX]; + sprintf(test_abspath, "/bin/tests/%s", test_argv[0]); + execvp(test_abspath, test_argv); +} + +static void run_test(int n, char *test_cmd_line) { + int child = fork(); + if (child == 0) { + exec_test(test_cmd_line); + // If the exec returns something went wrong + exit(127); + } + + if (child < 0) { + fprintf(STDERR_FILENO, "fork: %s\n", strerror(errno)); + exit(EXIT_FAILURE); + } + + int status; + waitpid(child, &status, 0); + + int success = WIFEXITED(status) && WEXITSTATUS(status) == 0; + if (success) { + test_ok(n, success, NULL); + } else { + if (WIFSIGNALED(status)) { + test_ok(n, success, "Signal: %d", WSTOPSIG(status)); + } else { + test_ok(n, success, "Exit: %d", WEXITSTATUS(status)); + } + } +} + +int runtests_main(int argc, char **argv) { + for (int i = 1; i < argc; i++) { + if (strncmp(argv[i], "--help", 6) == 0) { + printf("Usage: %s [--help] [TEST]...\n", argv[0]); + printf("Run one, more, or all available tests\n"); + printf(" --help display this help and exit\n"); + exit(EXIT_SUCCESS); + } + } + + // TODO: capture test output + int devnull = open("/dev/null", O_RDONLY, 0); + if (devnull < 0) { + fprintf(STDERR_FILENO, "open: /dev/null: %s\n", strerror(errno)); + exit(EXIT_FAILURE); + } + test_out_fd = test_err_fd = devnull; + + if (argc > 1) { + tests = argv + 1; + testsc = argc - 1; + } + + test_out("1..%d", testsc); + + char *test_argv[32]; + for (int i = 0; i < testsc; i++) { + run_test(i + 1, tests[i]); + } + + // We are running as init + if (init) { + outports(SHUTDOWN_PORT, 0x2000); + } + + return 0; +} + +int main(int argc, char **argv) +{ + // Are we the init process + init = getpid() == 1; + if (init) { + pid_t runtests = fork(); + if (runtests) { + while (1) { wait(NULL); } + } + } + + return runtests_main(argc, argv); +} diff --git a/scripts/tapview b/scripts/tapview new file mode 100755 index 00000000..53dbae44 --- /dev/null +++ b/scripts/tapview @@ -0,0 +1,290 @@ +#! /bin/sh +# tapview - a TAP (Test Anything Protocol) viewer in pure POSIX shell +# +# This code is intended to be embedded in your project. The author +# grants permission for it to be distributed under the prevailing +# license of your project if you choose, provided that license is +# OSD-compliant; otherwise the following SPDX tag incorporates the +# MIT No Attribution license by reference. +# +# SPDX-FileCopyrightText: (C) Eric S. Raymond +# SPDX-License-Identifier: MIT-0 +# +# This version shipped on 2024-02-05. +# +# A newer version may be available at https://gitlab.com/esr/tapview; +# check the ship date oagainst the commit list there to see if it +# might be a good idea to update. + +OK="." +FAIL="F" +SKIP="s" +TODO_NOT_OK="x" +TODO_OK="u" +LF=' +' + +ship_char() { + # shellcheck disable=SC2039 + printf '%s' "$1" # https://www.etalabs.net/sh_tricks.html +} + +ship_line() { + report="${report}${1}$LF" +} + +ship_error() { + # Terminate dot display and bail out with error + if [ "${testcount}" -gt 0 ] + then + echo "" + fi + report="${report}${1}$LF" + echo "${report}" + exit 1 +} + +testcount=0 +failcount=0 +skipcount=0 +todocount=0 +status=0 +report="" +ln=0 +state=plaintext + +# shellcheck disable=SC2086 +context_get () { printenv "ctx_${1}${depth}"; } +context_set () { export "ctx_${1}${depth}=${2}"; } + +context_push () { + context_set plan "" + context_set count 0 + context_set test_before_plan no + context_set test_after_plan no + context_set expect "" + context_set bail no + context_set strict no +} + +context_pop () { + if [ "$(context_get count)" -gt 0 ] && [ -z "$(context_get plan)" ] + then + ship_line "Missing a plan at line ${ln}." + status=1 + elif [ "$(context_get test_before_plan)" = "yes" ] && [ "$(context_get test_after_plan)" = "yes" ] + then + ship_line "A plan line may only be placed before or after all tests." + status=1 + elif [ "$(context_get plan)" != "" ] && [ "$(context_get expect)" -gt "$(context_get count)" ] + then + ship_line "Expected $(context_get expect) tests but only ${testcount} ran." + status=1 + elif [ "$(context_get plan)" != "" ] && [ "$(context_get expect)" -lt "$(context_get count)" ] + then + ship_line "${testcount} ran but $(context_get expect) expected." + status=1 + fi +} + +directive () { + case "$1" in + *[[:space:]]#[[:space:]]*$2*) return 0;; + *) return 1;; + esac +} + +depth=0 +context_push +while read -r line +do + ln=$((ln + 1)) + IFS=" " + # shellcheck disable=SC2086 + set -- $line + tok1="$1" + tok2="$2" + tok3="$3" + IFS="" + # Ignore blank lines and comments + if [ -z "$tok1" ] || [ "$tok1" = '#' ] + then + continue + fi + # Process bailout + if [ "$tok1" = "Bail" ] && [ "$tok2" = "out!" ] + then + ship_line "$line" + status=2 + break + fi + # Use the current indent to choose a scope level + leading_spaces="${line%%[! ]*}" + indent=${#leading_spaces} + if [ "${indent}" -lt "${depth}" ] + then + context_pop + depth="${indent}" + elif [ "${indent}" -gt "${depth}" ] + then + depth="${indent}" + context_push + fi + # Process a plan line + case "$tok1" in + [0123456789]*) + case "$tok1" in + 1[.][.]*) + if [ "$(context_get plan)" != "" ] + then + ship_error "tapview: cannot have more than one plan line." + fi + if directive "$line" [Ss][Kk][Ii][Pp] + then + ship_line "$line" + echo "${report}" + exit 1 # Not specified in the standard whether this should exit 1 or 0 + fi + context_set plan "${line}" + context_set expect "${tok1##[1][.][.]}" + continue + ;; + *) + echo "Ill-formed plan line at ${ln}" + exit 1 + ;; + esac + ;; esac + # Check for test point numbers out-of-order with the sequence (TAP 14) + testpoint="" + case "$tok1" in + ok) testpoint="$tok2";; + not) testpoint="$tok3";; + esac + case "$testpoint" in + *[0123456789]*) + if [ "${testpoint}" != "" ] && [ "$(context_get expect)" != "" ] && [ "${testpoint}" -gt "$(context_get expect)" ] + then + ship_error "tapview: testpoint number ${testpoint} is out of range for plan $(context_get plan)." + fi + ;; esac + # Process an ok line + if [ "$tok1" = "ok" ] + then + context_set count $(($(context_get count) + 1)) + testcount=$((testcount + 1)) + if [ "$(context_get plan)" = "" ] + then + context_set test_before_plan yes + else + context_set test_after_plan yes + fi + if directive "$line" [Tt][Oo][Dd][Oo] + then + ship_char ${TODO_OK} + ship_line "$line" + todocount=$((todocount + 1)) + elif directive "$line" [Ss][Kk][Ii][Pp] + then + ship_char ${SKIP} + ship_line "$line" + skipcount=$((skipcount + 1)) + else + ship_char ${OK} + fi + state=plaintext + continue + fi + # Process a not-ok line + if [ "$tok1" = "not" ] && [ "$tok2" = "ok" ] + then + context_set count $(($(context_get count) + 1)) + testcount=$((testcount + 1)) + if [ "$(context_get plan)" = "" ] + then + context_set test_before_plan yes + else + context_set test_after_plan yes + fi + if directive "$line" [Ss][Kk][Ii][Pp] + then + ship_char "${SKIP}" + state=plaintext + skipcount=$((skipcount + 1)) + continue + fi + if directive "$line" [Tt][Oo][Dd][Oo] + then + ship_char ${TODO_NOT_OK} + state=plaintext + todocount=$((todocount + 1)) + continue + fi + ship_char "${FAIL}" + ship_line "$line" + state=plaintext + failcount=$((failcount + 1)) + status=1 + if [ "$(context_get bail)" = yes ] + then + ship_line "Bailing out on line ${ln} due to +bail pragma." + break + fi + continue + fi + # Process a TAP 14 pragma + case "$line" in + pragma*) + case "$line" in + *+bail*) context_set bail yes;; + *-bail*) context_set bail yes;; + *+strict*) context_set strict yes;; + *-strict*) context_set strict yes;; + *) ship_line "Pragma '$line' ignored";; + esac + continue + ;; + esac + # shellcheck disable=SC2166 + if [ "${state}" = "yaml" ] + then + ship_line "$line" + if [ "$tok1" = "..." ] + then + state=plaintext + else + continue + fi + elif [ "$tok1" = "---" ] + then + ship_line "$line" + state=yaml + continue + fi + # Any line that is not a valid plan, test result, pragma, + # or comment lands here. + if [ "$(context_get strict)" = yes ] + then + ship_line "Bailing out on line ${ln} due to +strict pragma" + status=1 + break + fi +done + +/bin/echo "" +depth=0 +context_pop +report="${report}${testcount} tests, ${failcount} failures" +if [ "$todocount" != 0 ] +then + report="${report}, ${todocount} TODOs" +fi +if [ "$skipcount" != 0 ] +then + report="${report}, ${skipcount} SKIPs" +fi + +echo "${report}." +exit "${status}" + +# end