diff --git a/.github/workflows/riscv-openocd-ci.yml b/.github/workflows/riscv-openocd-ci.yml new file mode 100644 index 0000000000..aec94d1363 --- /dev/null +++ b/.github/workflows/riscv-openocd-ci.yml @@ -0,0 +1,63 @@ +name: riscv-openocd-ci +on: [push] + +jobs: + + run-riscv-tests: + runs-on: ubuntu-18.04 + steps: + + - name: Install required pkgs + run: | + sudo apt-get install libtool pkg-config autoconf automake libusb-1.0 libftdi1 lcov device-tree-compiler pylint3 + sudo python3 -m pip install pexpect + + - name: Checkout OpenOCD + uses: actions/checkout@v2 + + - name: Build OpenOCD + run: bash tools/riscv-openocd-ci/build_openocd.sh + + - name: Checkout & build Spike + run: bash tools/riscv-openocd-ci/build_spike.sh + + - name: Download RISC-V toolchain + run: bash tools/riscv-openocd-ci/download_toolchain.sh + + - name: Update env. variables + # Change RISCV and PATH env variables. Needed for the next step. + run: | + echo "`pwd`/tools/riscv-openocd-ci/work/install/bin" >> $GITHUB_PATH + echo "RISCV=`pwd`/tools/riscv-openocd-ci/work/install" >> $GITHUB_ENV + + - name: Checkout & run riscv-tests + run: bash tools/riscv-openocd-ci/run_tests.sh + + - name: Process test results + # If at least one test failed (or ended with an exception), this step fails. + run: | + cd tools/riscv-openocd-ci + python3 process_test_results.py --log-dir work/riscv-tests/debug/logs --output-dir work/results/logs + + - name: Store test results + # Run even if there was a failed test + if: ${{ success() || failure() }} + uses: actions/upload-artifact@v2 + with: + name: test-results + path: tools/riscv-openocd-ci/work/results/logs + + - name: Collect OpenOCD code coverage + # Run even if there was a failed test + if: ${{ success() || failure() }} + run: | + lcov --capture --directory . --output-file tools/riscv-openocd-ci/work/openocd-coverage.info + genhtml tools/riscv-openocd-ci/work/openocd-coverage.info --output-directory tools/riscv-openocd-ci/work/results/openocd-coverage + + - name: Store OpenOCD code coverage + # Run even if there was a failed test + if: ${{ success() || failure() }} + uses: actions/upload-artifact@v2 + with: + name: openocd-coverage + path: tools/riscv-openocd-ci/work/results/openocd-coverage diff --git a/.gitignore b/.gitignore index 41573d881d..b47d516a1e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,10 @@ *.la *.in +# coverage files (gcov) +*.gcda +*.gcno + # generated source files src/jtag/minidriver_imp.h src/jtag/jtag_minidriver.h diff --git a/src/target/riscv/batch.c b/src/target/riscv/batch.c index 43f2ffb8c0..073f80888f 100644 --- a/src/target/riscv/batch.c +++ b/src/target/riscv/batch.c @@ -82,8 +82,6 @@ int riscv_batch_run(struct riscv_batch *batch) return ERROR_OK; } - keep_alive(); - riscv_batch_add_nop(batch); for (size_t i = 0; i < batch->used_scans; ++i) { @@ -96,11 +94,15 @@ int riscv_batch_run(struct riscv_batch *batch) jtag_add_runtest(batch->idle_count, TAP_IDLE); } + keep_alive(); + if (jtag_execute_queue() != ERROR_OK) { LOG_ERROR("Unable to execute JTAG queue"); return ERROR_FAIL; } + keep_alive(); + if (bscan_tunnel_ir_width != 0) { /* need to right-shift "in" by one bit, because of clock skew between BSCAN TAP and DM TAP */ for (size_t i = 0; i < batch->used_scans; ++i) diff --git a/src/target/riscv/riscv-013.c b/src/target/riscv/riscv-013.c index ace362929b..4b9bd93e12 100644 --- a/src/target/riscv/riscv-013.c +++ b/src/target/riscv/riscv-013.c @@ -591,6 +591,8 @@ static int dmi_op_timeout(struct target *target, uint32_t *data_in, return ERROR_FAIL; } + keep_alive(); + time_t start = time(NULL); /* This first loop performs the request. Note that if for some reason this * stays busy, it is actually due to the previous access. */ @@ -1307,8 +1309,6 @@ static int register_write_direct(struct target *target, unsigned number, LOG_DEBUG("{%d} %s <- 0x%" PRIx64, riscv_current_hartid(target), gdb_regno_name(number), value); - keep_alive(); - int result = register_write_abstract(target, number, value, register_size(target, number)); if (result == ERROR_OK || !has_sufficient_progbuf(target, 2) || @@ -2730,6 +2730,7 @@ static int read_memory_bus_v1(struct target *target, target_addr_t address, next_read); return ERROR_FAIL; } + keep_alive(); dmi_status_t status = dmi_scan(target, NULL, &value, DMI_OP_READ, sbdata[j], 0, false); if (status == DMI_STATUS_BUSY) @@ -2745,7 +2746,6 @@ static int read_memory_bus_v1(struct target *target, target_addr_t address, } next_read = address + i * size + j * 4; } - keep_alive(); } uint32_t sbcs_read = 0; @@ -3507,7 +3507,6 @@ static int read_memory_progbuf(struct target *target, target_addr_t address, uint8_t *buffer_i = buffer; for (uint32_t i = 0; i < count; i++, address_i += increment, buffer_i += size) { - keep_alive(); /* TODO: This is much slower than it needs to be because we end up * writing the address to read for every word we read. */ result = read_memory_progbuf_inner(target, address_i, size, count_i, buffer_i, increment); diff --git a/src/target/riscv/riscv.c b/src/target/riscv/riscv.c index 8cfb2275ad..42b0483587 100644 --- a/src/target/riscv/riscv.c +++ b/src/target/riscv/riscv.c @@ -2227,6 +2227,8 @@ int riscv_openocd_poll(struct target *target) int riscv_openocd_step(struct target *target, int current, target_addr_t address, int handle_breakpoints) { + RISCV_INFO(r); + LOG_DEBUG("stepping rtos hart"); if (!current) @@ -2249,8 +2251,11 @@ int riscv_openocd_step(struct target *target, int current, target->state = TARGET_RUNNING; target_call_event_callbacks(target, TARGET_EVENT_RESUMED); + target->state = TARGET_HALTED; - target->debug_reason = DBG_REASON_SINGLESTEP; + /* Read real debug reason from the target. Do not presume the target halted due + * to the single-step. There may be a higher-priority halt cause, e.g. a breakpoint. */ + set_debug_reason(target, r->current_hartid); target_call_event_callbacks(target, TARGET_EVENT_HALTED); return out; } diff --git a/tools/riscv-openocd-ci/.gitignore b/tools/riscv-openocd-ci/.gitignore new file mode 100644 index 0000000000..b8f99f5be5 --- /dev/null +++ b/tools/riscv-openocd-ci/.gitignore @@ -0,0 +1 @@ +work diff --git a/tools/riscv-openocd-ci/README.md b/tools/riscv-openocd-ci/README.md new file mode 100644 index 0000000000..7e5cfb11a8 --- /dev/null +++ b/tools/riscv-openocd-ci/README.md @@ -0,0 +1,21 @@ +# CI for riscv-openocd + +This directory contains a set of scripts that automatically +run [riscv-tests/debug](https://github.com/riscv/riscv-tests/tree/master/debug) +against riscv-openocd. + +The scripts are intended to be called automatically by Github +Actions as a means of testing & continuous integration for riscv-openocd. + +The scripts perform these actions: + +- Build OpenOCD from source +- Checkout and build Spike (RISC-V ISA simulator) from source +- Download a pre-built RISC-V toolchain +- Use these components together to run + [riscv-tests/debug](https://github.com/riscv/riscv-tests/tree/master/debug) +- Process the test results +- Collect code coverage for OpenOCD + +See [.github/workflows](../../.github/workflows) for an example of how this is +used in practice. diff --git a/tools/riscv-openocd-ci/build_openocd.sh b/tools/riscv-openocd-ci/build_openocd.sh new file mode 100644 index 0000000000..8e5fb4b9a5 --- /dev/null +++ b/tools/riscv-openocd-ci/build_openocd.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +INSTALL_DIR=`pwd`/tools/riscv-openocd-ci/work/install + +# Fail on first error. +set -e + +# Echo commands. +set -o xtrace + +# Assuming OpenOCD source is already checked-out in the current workdir. + +# Show revision info +git --no-pager log --no-walk --pretty=short + +./bootstrap + +# Enable most frequently used JTAG drivers. +# Allow for code coverage collection. +./configure \ + --enable-remote-bitbang \ + --enable-jtag_vpi \ + --enable-ftdi \ + --prefix=$INSTALL_DIR \ + CFLAGS="-O0 --coverage -fprofile-arcs -ftest-coverage" \ + CXXFLAGS="-O0 --coverage -fprofile-arcs -ftest-coverage" \ + LDFLAGS="-fprofile-arcs -lgcov" + +# Patch OpenOCD so that coverage is recorded also when terminated +# by a signal. +git apply tools/riscv-openocd-ci/patches/openocd_gcov_flush.patch + +# Build and install OpenOCD +make clean # safety +make -j`nproc` +make install + +# Check that OpenOCD runs +$INSTALL_DIR/bin/openocd --version diff --git a/tools/riscv-openocd-ci/build_spike.sh b/tools/riscv-openocd-ci/build_spike.sh new file mode 100644 index 0000000000..8964946563 --- /dev/null +++ b/tools/riscv-openocd-ci/build_spike.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +CHECKOUT_DIR=`pwd`/tools/riscv-openocd-ci/work/riscv-isa-sim +INSTALL_DIR=`pwd`/tools/riscv-openocd-ci/work/install + +# Fail on first error. +set -e + +# Echo commands. +set -o xtrace + +# Checkout Spike. +mkdir -p "$CHECKOUT_DIR" +cd "$CHECKOUT_DIR" +git clone --depth=1 --recursive https://github.com/riscv/riscv-isa-sim.git . + +# Show revision info +git --no-pager log --no-walk --pretty=short + +# Build Spike +mkdir build +cd build +bash ../configure --prefix=$INSTALL_DIR +make clean # safety +make -j`nproc` +make install + +# Check that Spike runs +$INSTALL_DIR/bin/spike --help diff --git a/tools/riscv-openocd-ci/download_toolchain.sh b/tools/riscv-openocd-ci/download_toolchain.sh new file mode 100644 index 0000000000..a1332e9491 --- /dev/null +++ b/tools/riscv-openocd-ci/download_toolchain.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# Toolchain builds provided by Embecosm (buildbot.embecosm.com) +# TOOLCHAIN_URL="https://buildbot.embecosm.com/job/riscv32-gcc-ubuntu1804" +# TOOLCHAIN_URL+="/25/artifact/riscv32-embecosm-gcc-ubuntu1804-20201108.tar.gz" +# TOOLCHAIN_PREFIX=riscv32-unknown-elf- + +# Toolchain builds from "The xPack Project" +# (https://xpack.github.io/riscv-none-embed-gcc/) +TOOLCHAIN_URL="https://github.com/xpack-dev-tools/riscv-none-embed-gcc-xpack/" +TOOLCHAIN_URL+="releases/download/v10.1.0-1.1/" +TOOLCHAIN_URL+="xpack-riscv-none-embed-gcc-10.1.0-1.1-linux-x64.tar.gz" +TOOLCHAIN_PREFIX=riscv-none-embed- + +ARCHIVE_NAME=${TOOLCHAIN_URL##*/} +DOWNLOAD_DIR=`pwd`/tools/riscv-openocd-ci/work +INSTALL_DIR=`pwd`/tools/riscv-openocd-ci/work/install + +# Fail on first error. +set -e + +# Echo commands. +set -o xtrace + +# Download the toolchain. +# Use a pre-built toolchain binaries provided by Embecosm: https://buildbot.embecosm.com/ +mkdir -p "$DOWNLOAD_DIR" +cd "$DOWNLOAD_DIR" +wget --progress dot:mega "$TOOLCHAIN_URL" + +# Extract +mkdir -p "$INSTALL_DIR" +cd "$INSTALL_DIR" +tar xvf "$DOWNLOAD_DIR/$ARCHIVE_NAME" --strip-components=1 + +cd "$INSTALL_DIR/bin" + +# Make symlinks so that the tools are accessible as riscv64-unknown-elf-* +if [ "$TOOLCHAIN_PREFIX" != "riscv64-unknown-elf-" ]; then + find . -name "$TOOLCHAIN_PREFIX*" | while read F; do + ln -s $F $(echo $F | sed -e "s/$TOOLCHAIN_PREFIX/riscv64-unknown-elf-/"); + done +fi + +# Check that the compiler and debugger run +./riscv64-unknown-elf-gcc --version +./riscv64-unknown-elf-gdb --version + diff --git a/tools/riscv-openocd-ci/patches/openocd_gcov_flush.patch b/tools/riscv-openocd-ci/patches/openocd_gcov_flush.patch new file mode 100644 index 0000000000..f8a0f3f93c --- /dev/null +++ b/tools/riscv-openocd-ci/patches/openocd_gcov_flush.patch @@ -0,0 +1,15 @@ +diff --git a/src/server/server.c b/src/server/server.c +index 4e970fa8f..43bd9fb2e 100644 +--- a/src/server/server.c ++++ b/src/server/server.c +@@ -725,6 +725,10 @@ void server_free(void) + + void exit_on_signal(int sig) + { ++ /* dump coverage before being killed by the signal ++ * (otherwise gcov's *.gcda files would not be created) */ ++ void __gcov_flush(void); ++ __gcov_flush(); + #ifndef _WIN32 + /* bring back default system handler and kill yourself */ + signal(sig, SIG_DFL); diff --git a/tools/riscv-openocd-ci/process_test_results.py b/tools/riscv-openocd-ci/process_test_results.py new file mode 100644 index 0000000000..f34e9b6c14 --- /dev/null +++ b/tools/riscv-openocd-ci/process_test_results.py @@ -0,0 +1,172 @@ + +from glob import glob +import logging +from logging import info, error +import os +import re +import shutil +import sys + +KNOWN_RESULTS = ["pass", "fail", "not_applicable", "exception"] + + +def info_box(msg): + """Display an emphasized message - print an ASCII box around it. """ + box = " +==" + ("=" * len(msg)) + "==+" + info("") + info(box) + info(" | " + msg + " |") + info(box) + info("") + + +def parse_args(): + """Process command-line arguments. """ + import argparse + parser = argparse.ArgumentParser("Process logs from riscv-tests/debug") + parser.add_argument("--log-dir", required=True, help="Directory where logs from RISC-V debug tests are stored") + parser.add_argument("--output-dir", required=True, help="Directory where put post-processed logs") + return parser.parse_args() + + +def process_test_logs(log_dir, output_dir): + """Process all logs from the testing. """ + + assert os.path.isdir(log_dir) + os.makedirs(output_dir, exist_ok=True) + + # process log files + file_pattern = os.path.join(log_dir, "*.log") + log_files = sorted(glob(file_pattern)) + + if not len(log_files): + # Did not find any *.log. + # Either the tests did not start at all or a wrong log directory was specified. + raise RuntimeError("No log files (*.log) in directory {}".format(log_dir)) + + tests = [] + for lf in log_files: + target, result = process_one_log(lf) + copy_one_log(lf, result, output_dir) + tests += [{"log": lf, "target": target, "result": result}] + + return tests + + +def process_one_log(log_file): + """Parse a single log file, extract required pieces from it. """ + assert os.path.isfile(log_file) + target = None + result = None + # Find target name and the test result in the log file + for line in open(log_file, "r"): + target_match = re.match(r"^Target: (\S+)$", line) + if target_match is not None: + target = target_match.group(1) + result_match = re.match(r"^Result: (\S+)$", line) + if result_match is not None: + result = result_match.group(1) + if result not in KNOWN_RESULTS: + msg = ("Unknown test result '{}' in file {}. Expected one of: {}" + .format(result, log_file, KNOWN_RESULTS)) + raise RuntimeError(msg) + + if target is None: + raise RuntimeError("Could not find target name in log file {}".format(log_file)) + if result is None: + raise RuntimeError("Could not find test result in log file {}".format(log_file)) + + return target, result + + +def copy_one_log(log_file, result, output_dir): + """Copy the log to a sub-folder based on the result. """ + target_dir = os.path.join(output_dir, result) + os.makedirs(target_dir, exist_ok=True) + assert os.path.isdir(target_dir) + shutil.copy2(log_file, target_dir) + + +def print_aggregated_results(tests): + """Print the tests grouped by the result. Print also pass/fail/... counts.""" + + def _filter_tests(tests, target=None, result=None): + tests_out = tests + if target is not None: + tests_out = filter(lambda t: t["target"] == target, tests_out) + if result is not None: + tests_out = filter(lambda t: t["result"] == result, tests_out) + return list(tests_out) + + # Print lists of passed/failed/... tests + outcomes = { + "Passed tests": "pass", + "Not applicable tests": "not_applicable", + "Failed tests": "fail", + "Tests ended with exception": "exception", + } + for caption, result in outcomes.items(): + info_box(caption) + tests_filtered = _filter_tests(tests, result=result) + for t in tests_filtered: + name = os.path.splitext(os.path.basename(t["log"]))[0] + info(name) + if not tests_filtered: + info("(none)") + + target_names = sorted(set([t["target"] for t in tests])) + + # Print summary - passed/failed/... counts, for each target and total + + info_box("Summary") + + def _print_row(target, total, num_pass, num_na, num_fail, num_exc): + info("{:<25} {:<10} {:<10} {:<10} {:<10} {:<10}".format(target, total, num_pass, num_na, num_fail, num_exc)) + + _print_row("Target", "# tests", "Pass", "Not_appl.", "Fail", "Exception") + _print_row("-----", "-----", "-----", "-----", "-----", "-----") + sum_pass = sum_na = sum_fail = sum_exc = 0 + for tn in target_names: + t_pass = len(_filter_tests(tests, target=tn, result="pass")) + t_na = len(_filter_tests(tests, target=tn, result="not_applicable")) + t_fail = len(_filter_tests(tests, target=tn, result="fail")) + t_exc = len(_filter_tests(tests, target=tn, result="exception")) + t_sum = len(_filter_tests(tests, target=tn)) + assert t_sum == t_pass + t_na + t_fail + t_exc # self-check + _print_row(tn, t_sum, t_pass, t_na, t_fail, t_exc) + sum_pass += t_pass + sum_na += t_na + sum_fail += t_fail + sum_exc += t_exc + assert len(tests) == sum_pass + sum_na + sum_fail + sum_exc # self-check + _print_row("-----", "-----", "-----", "-----", "-----", "-----") + _print_row("All targets:", len(tests), sum_pass, sum_na, sum_fail, sum_exc) + _print_row("-----", "-----", "-----", "-----", "-----", "-----") + + any_failed = (sum_fail + sum_exc) > 0 + return any_failed + + +def main(): + args = parse_args() + + # Use absolute paths. + args.log_dir = os.path.abspath(args.log_dir) + args.output_dir = os.path.abspath(args.output_dir) + + # Process the log files and print results. + tests = process_test_logs(args.log_dir, args.output_dir) + any_failed = print_aggregated_results(tests) + + # The overall exit code. + exit_code = 1 if any_failed else 0 + if any_failed: + error("Encountered failed test(s). Exiting with non-zero code.") + else: + info("Success - no failed tests encountered.") + return exit_code + + +if __name__ == '__main__': + logging.getLogger().setLevel(logging.INFO) + sys.exit(main()) diff --git a/tools/riscv-openocd-ci/run_tests.sh b/tools/riscv-openocd-ci/run_tests.sh new file mode 100644 index 0000000000..284b29217f --- /dev/null +++ b/tools/riscv-openocd-ci/run_tests.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +CHECKOUT_DIR=`pwd`/tools/riscv-openocd-ci/work/riscv-tests + +# Fail on first error. +set -e + +# Echo commands. +set -o xtrace + +# Checkout riscv-tests. +mkdir -p "$CHECKOUT_DIR" +cd "$CHECKOUT_DIR" +git clone --depth=1 --recursive https://github.com/riscv/riscv-tests . + +# Show revision info +git --no-pager log --no-walk --pretty=short + +# Run the debug tests. +# Do not stop even on a failed test. +# Use slightly more jobs than CPUs. Observed that this still speeds up the testing. +cd debug +JOBS=$(($(nproc) + 2)) +make -k -j$JOBS all || true