From c059620b095e1d6db6573ed2dadb123834bbb6e6 Mon Sep 17 00:00:00 2001
From: Wesley Moore <wes@wezm.net>
Date: Mon, 10 Feb 2025 12:03:11 +1000
Subject: [PATCH] Add a byte to hex function with unit test

---
 Makefile             |  14 ++-
 calc.s               |  13 ++-
 hex.s                |  29 +++++
 tests/btohex.s       |  20 ++++
 tests/test_btohex.sh |  10 ++
 tests/unittest       | 272 +++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 356 insertions(+), 2 deletions(-)
 create mode 100644 hex.s
 create mode 100644 tests/btohex.s
 create mode 100644 tests/test_btohex.sh
 create mode 100755 tests/unittest

diff --git a/Makefile b/Makefile
index 52cec4c..d53b7ae 100644
--- a/Makefile
+++ b/Makefile
@@ -3,14 +3,26 @@ AS=riscv64-unknown-elf-as
 ASFLAGS=-g -mabi=ilp32e -march=rv32ec
 CFLAGS=$(ASFLAGS)
 LD=riscv64-unknown-elf-ld
+export JQ?=jaq
+export RV32EMU?=$(HOME)/Source/github.com/sysprog21/rv32emu/build/rv32emu
 
 all: calc.elf
 
+check: tests
+	@tests/unittest
+
 hello.elf: hello.o
 	$(LD) -m elf32lriscv $^ -o $@
 
-calc.elf: calc.o
+calc.elf: hex.o calc.o
+	$(LD) -m elf32lriscv -T link.ld $^ -o $@
+
+tests: tests/btohex.elf
+
+tests/btohex.elf: hex.o tests/btohex.o
 	$(LD) -m elf32lriscv -T link.ld $^ -o $@
 
 %.o : %.s
 	$(AS) $(ASFLAGS) $< -o $@
+
+.PHONY: check tests
diff --git a/calc.s b/calc.s
index f8c2dbd..0504e50 100644
--- a/calc.s
+++ b/calc.s
@@ -20,7 +20,7 @@ regnames:
 
 .section .bss
 buf: .skip 20        # room for 20 byte string
-                     # x10: ABCDEFG\n is 23 chars
+                     # x10: ABCDEFGH\n is 14 chars
 
 .section .data
 buflen: .byte 0      # length of buf string
@@ -160,3 +160,14 @@ memcpy_loop:
   j memcpy_loop
 memcpy_done:
   ret
+
+# Write hex representation into buffer
+#    arguments:
+#       a0: value to format
+#       a1: address of buffer to write to
+#   temporaries used:
+#       t0
+#    return:
+#       none
+tohex:
+  
diff --git a/hex.s b/hex.s
new file mode 100644
index 0000000..d5f0c2a
--- /dev/null
+++ b/hex.s
@@ -0,0 +1,29 @@
+.global btohex
+
+.section .rodata
+hexchars:
+  .ascii "0123456789ABCDEF"
+
+.text
+
+# Convert byte to ASCII hex
+#    arguments:
+#       a0: value to format
+#   temporaries used:
+#       t0, t1
+#    return:
+#       a0: value in ASCII hex
+btohex:
+  andi t0, a0, 0xF      # Mask off lower nybble
+  la   a1, hexchars     # load address of hexchars
+  add  a2, a1, t0       # offset to nybble
+  lbu  t0, 0(a2)        # load hex char at offset
+  srli t1, a0, 4        # shift upper nybble down
+  andi t1, t1, 0xF      # mask off upper nybble
+  add  a2, a1, t1       # offset to nybble
+  lbu  t1, 0(a2)        # load hex char at offset
+  mv a0, t1             # copy upper char to a0
+  slli a0, a0, 8        # shuft upper char up
+  or   a0, a0, t0       # OR the lower char into a0
+  ret
+
diff --git a/tests/btohex.s b/tests/btohex.s
new file mode 100644
index 0000000..22a9adf
--- /dev/null
+++ b/tests/btohex.s
@@ -0,0 +1,20 @@
+# Test for btohex
+
+.org 0
+# Provide program starting address to linker
+.global _start
+
+.extern btohex
+
+/* newlib system calls */
+.set SYSEXIT,  93
+
+.text
+_start:
+    li a0, 0xA5
+    jal btohex
+    mv a1, a0
+
+    li t0, SYSEXIT      # "exit" syscall
+    la a0, 0            # Use 0 return code
+    ecall               # invoke syscall to terminate the program
diff --git a/tests/test_btohex.sh b/tests/test_btohex.sh
new file mode 100644
index 0000000..c39f321
--- /dev/null
+++ b/tests/test_btohex.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+test_btohex() {
+  # FIXME: Remove grep when this bug is fixed:
+  # https://github.com/sysprog21/rv32emu/issues/561
+  result=$("${RV32EMU}" -d - -q tests/btohex | grep -v '^\d' | "${JQ}" .x11)
+
+  test $? -eq 0 && test "${result}" -eq 16693 # 16693 is A5 in ASCII
+}
+
diff --git a/tests/unittest b/tests/unittest
new file mode 100755
index 0000000..2f0d3ae
--- /dev/null
+++ b/tests/unittest
@@ -0,0 +1,272 @@
+#!/bin/sh
+#
+# unittest - unit tests framework for shell scripts.
+#
+# https://github.com/macie/unittest.sh
+#
+# Copyright (c) 2014-2023 Maciej Żok <maciek.zok@gmail.com>
+# MIT License (http://opensource.org/licenses/MIT)
+
+
+#
+#   MESSAGES
+#
+
+unittest__print_result() {
+    # Write test status message to stdout
+    # $1 - test location
+    # $2 - test status
+    # prefix: utt9t_
+    utt9t_color_default=''
+    utt9t_color_location=''
+    utt9t_color_status=''
+
+    if { [ -n "${CLICOLOR_FORCE}" ] && [ "${CLICOLOR_FORCE}" -ne 0 ]; } || { [ -t 1 ] && [ -z "${NO_COLOR}" ]; }; then  # stdout is interactive terminal
+        utt9t_color_default='\033[0m'
+        case $2 in
+            PASS)  # location: default; status: green
+                utt9t_color_location='\033[0m'
+                utt9t_color_status='\033[32m'
+                ;;
+            FAIL)  # location: red; status: white on red
+                utt9t_color_location='\033[31m'
+                utt9t_color_status='\033[97;41m'
+                ;;
+            SKIP)  # location: gray; status: gray
+                utt9t_color_location='\033[37m'
+                utt9t_color_status='\033[37m'
+                ;;
+        esac
+    fi
+
+    printf "${utt9t_color_location}%s\t${utt9t_color_status}%s${utt9t_color_default}\n" "$1" "$2"
+
+    unset -v utt9t_color_default utt9t_color_location utt9t_color_status
+    return 0
+}
+
+unittest__print_debug() {
+    # $1 - category
+    # $2-... - paragraphs
+    # prefix: utt13o_
+    utt13o_color=''
+    utt13o_color_default=''
+
+    if { [ -n "${CLICOLOR_FORCE}" ] && [ "${CLICOLOR_FORCE}" -ne 0 ]; } || { [ -t 2 ] && [ -z "${NO_COLOR}" ]; }; then  # stderr is interactive terminal
+        utt13o_color='\033[34m'  # blue
+        utt13o_color_default='\033[0m'
+        utt13o_color_quote='\033[37m'  # gray
+    fi
+
+    printf "\n${utt13o_color}-- %s${utt13o_color_default}\n\n" "$1" >&2
+
+    shift 1
+    for utt13o_paragraph in "$@"; do
+        # shellcheck disable=SC2059
+        case ${utt13o_paragraph} in
+            ' '*) printf "${utt13o_color_quote}" >&2 ;;
+            *)  ;;
+        esac
+        printf "%s${utt13o_color_default}\n\n" "${utt13o_paragraph}" >&2
+    done
+
+    unset -v utt13o_paragraph utt13o_color utt13o_color_default
+    return 0
+}
+
+#
+#   ASSERTIONS
+#
+
+# shellcheck disable=SC2317
+test() {
+    # same arguments as command test(1)
+    UT4e1_test_error_msg=$(/bin/test "$@" 2>&1)
+    case $? in
+        0)  ;;
+        1)
+            unittest__print_debug "FAILED TEST [${UNITTEST_CURRENT}]" \
+                'I expected:' \
+                "    test$(printf " '%s'" "$@")" \
+                'to be true, but the result was false.'
+            return 1
+            ;;
+        *)
+            unittest__print_debug "INVALID ASSERTION [${UNITTEST_CURRENT}]" \
+                'I tried to check' \
+                "    test$(printf " '%s'" "$@")" \
+                'but I got error with message:' \
+                "    ${UT4e1_test_error_msg}" \
+                'Did you use proper operator?' \
+                "Hint: Some operators requires specific type of values. Read 'man test' to learn more."
+            return 1
+            ;;
+    esac
+
+    return 0
+}
+
+
+#
+#   FUNCTIONS
+#
+
+##
+# Find files with tests (test_*.sh).
+# SYNOPSIS:
+#     unittest__test_files [directory...]
+# OPERANDS:
+#     directory - A pathname of directory to search in. If no directory is
+#         given, it will look for 'tests' directory inside current one. If
+#         a directory is '-', it will use stdin.
+# STDIN:
+#     (optional) List of directories to search in. Used when call with '-' argument.
+# STDOUT:
+#     List of 'test_*.sh' file paths.
+# STDERR:
+#     (optional) Debug/error message.
+# EXIT STATUS:
+#     0 - Successfully traversed all directories.
+#    >0 - An error occurred.
+# EXAMPLES:
+#     unittest__test_files ./unit_tests ./integration_tests
+#     ls ../ | unittest__test_files -
+# CAVEATS:
+#     Calling it inside pipeline without '-' will disregards standard input and
+#     use defaults instead.
+unittest__test_files() {
+    {
+        if [ "$1" = '-' ]; then
+            cat -
+        else
+            printf '%s\n' "$@"
+        fi
+     } |
+     while read -r unittest_dir; do
+         if ! find "${unittest_dir:-./}" -path "*${unittest_dir:-tests/}*" -name 'test_*.sh' 2>/dev/null; then
+             unittest__print_debug 'TESTS NOT FOUND' \
+                 "I was looking for 'test_*.sh' files inside '${unittest_dir:-tests/}' directory using:" \
+                 "    $ find \"${unittest_dir:-./}\" -path \"*${unittest_dir:-tests/}*\" -name 'test_*.sh' -print" \
+                 'but instead of files I got an error with message:' \
+                 "    $(find "${unittest_dir:-./}" -path "*${unittest_dir:-tests/}*" -name 'test_*.sh' -print 2>&1)"
+             unset -v unittest_dir
+             return 1
+         fi
+         unset -v unittest_dir
+     done
+
+     return $?
+}
+
+##
+# Run tests from given files.
+# STDIN: List of files.
+# STDOUT: Test name with status.
+# STDERR: (optional) Debug/error message.
+# EXIT STATUS:
+#     0 - All tests passed.
+#    >0 - Some tests failed.
+##
+unittest__run() {
+    # prefix: utt8r_
+
+    UNITTEST_STATUS=0
+    while read -r utt8r_testfile; do
+        (
+            utt8r_beforeAll=$(sed -n 's/^[ \t]*\(beforeAll\)[ \t]*(.*/\1/p' "${utt8r_testfile}")
+            utt8r_afterAll=$(sed -n 's/^[ \t]*\(afterAll\)[ \t]*(.*/\1/p' "${utt8r_testfile}")
+            utt8r_beforeEach=$(sed -n -e 's/^[ \t]*\(beforeEach\)[ \t]*(.*/\1/p' -e 's/^[ \t]*\(setUp\)[ \t]*(.*/\1/p' "${utt8r_testfile}")
+            utt8r_afterEach=$(sed -n -e 's/^[ \t]*\(afterEach\)[ \t]*(.*/\1/p' -e 's/^[ \t]*\(tearDown\)[ \t]*(.*/\1/p' "${utt8r_testfile}")
+            utt8r_tests=$(sed -n 's/^[ \t]*\(x\{0,1\}test_[^(=]*\)(.*/\1/p' "${utt8r_testfile}")
+
+			# shellcheck source=/dev/null
+            . "${utt8r_testfile}"
+            ${utt8r_beforeAll}
+            for _current_testcase in ${utt8r_tests}; do
+                UNITTEST_CURRENT="${utt8r_testfile#./}:${_current_testcase}"
+
+                case ${_current_testcase} in
+                    x*)
+                        unittest__print_result "${UNITTEST_CURRENT}" 'SKIP'
+                        ;;
+                    *)
+                        ${utt8r_beforeEach}
+                        # test result is status of last command in test
+                        if ${_current_testcase}; then
+                            unittest__print_result "${UNITTEST_CURRENT}" 'PASS'
+                        else
+                            # last command in test failed
+                            UNITTEST_STATUS=1
+                            unittest__print_result "${UNITTEST_CURRENT}" 'FAIL'
+                        fi
+                        ${utt8r_afterEach}
+                        ;;
+                esac
+            done
+            ${utt8r_afterAll}
+            unset -v UNITTEST_CURRENT
+            exit ${UNITTEST_STATUS}
+        )
+        UNITTEST_STATUS=$?
+    done
+    return ${UNITTEST_STATUS}
+}
+
+
+#
+#   MAIN ROUTINE
+#
+
+{
+    # Color output by default: on supported terminals when NO_COLOR is not set.
+    # Supported terminals are recognized based on TERM variable. When TERM is
+    # not set (for example inside CI environment) we assume that terminal is dumb.
+    if [ -z "${NO_COLOR}" ] && [ "$(TERM=${TERM:-dumb} tput colors)" -lt 8 ]; then
+        NO_COLOR='YES'
+    fi
+
+    case $# in
+        0)  # discovery mode
+            unittest__test_files | unittest__run
+            exit $?
+            ;;
+        1)
+            case $1 in
+                -h|--help)
+                    cat >&2 <<-'EOF'
+						unittest - unit tests framework for shell scripts.
+
+						Usage:
+						  unittest [options] [test_directory | test_file]
+
+						Options:
+						  -h, --help           Show this help and exit.
+						  -v, --version        Show version number and exit.
+
+						Without any arguments it will run all tests from 'tests' directory.
+						EOF
+                    exit 0
+                    ;;
+
+                -v|--version)
+                    printf 'unittest 23.11\n' >&2
+                    exit 0
+                    ;;
+                *)  # specified directory/file
+                    if [ -d "$1" ]; then
+                        unittest__test_files "$1" | unittest__run
+                        exit $?
+                    elif [ -f "$1" ]; then
+                        printf '%s\n' "$1" | unittest__run
+                        exit $?
+                    fi
+                    ;;
+            esac
+    esac
+
+    unittest__print_debug 'INVALID USAGE' \
+        "I cannot understand '$*' option. Did you want to use option or did you misspell file/directory?" \
+        'Hint: Find valid usage with:' \
+        '    $ unittest -h'
+    exit 64  # EX_USAGE
+}