From db3ffd40356796a5df78801e93503060bd4fe059 Mon Sep 17 00:00:00 2001 From: Eduardo Barthel Date: Thu, 18 May 2023 15:04:51 -0300 Subject: [PATCH] Add new Lua testing framework --- CHANGELOG.md | 1 + src/Makefile | 1 + src/clua-cartesi.cpp | 4 + src/machine.cpp | 1 - src/spec/all-tests.lua | 32 ++ src/spec/clua-tests.lua | 44 ++ src/spec/config-tests.lua | 1120 ++++++++++++++++++++++++++++++++++++ src/spec/htif-tests.lua | 202 +++++++ src/spec/keccak-tests.lua | 69 +++ src/spec/machine-tests.lua | 96 ++++ src/spec/step-tests.lua | 58 ++ src/spec/util/fs.lua | 55 ++ src/spec/util/lester.lua | 594 +++++++++++++++++++ 13 files changed, 2276 insertions(+), 1 deletion(-) create mode 100644 src/spec/all-tests.lua create mode 100644 src/spec/clua-tests.lua create mode 100644 src/spec/config-tests.lua create mode 100644 src/spec/htif-tests.lua create mode 100644 src/spec/keccak-tests.lua create mode 100644 src/spec/machine-tests.lua create mode 100644 src/spec/step-tests.lua create mode 100644 src/spec/util/fs.lua create mode 100644 src/spec/util/lester.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fc40d849..0ace7419d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated Lua version to 5.4 - Use Lua path environment variables specific for version 5.4 +- Refactored and added new Lua tests using a Lua testing framework ## [0.14.0] - 2023-05-03 ### Added diff --git a/src/Makefile b/src/Makefile index be448c691..2c1e4f190 100644 --- a/src/Makefile +++ b/src/Makefile @@ -410,6 +410,7 @@ test-jsonrpc: luacartesi jsonrpc ./tests/test-jsonrpc-server.sh ./jsonrpc-remote-cartesi-machine "$(LUA) ./cartesi-machine.lua" "$(LUA) ./cartesi-machine-tests.lua" "$(TEST_PATH)" "$(LUA)" test-scripts: luacartesi + $(LUA) spec/all-tests.lua for x in `find tests -maxdepth 1 -type f -name "*.lua"` ; do \ echo -n 'CTSICTSI' | $(LUA) $$x local || exit 1; \ done diff --git a/src/clua-cartesi.cpp b/src/clua-cartesi.cpp index a3c1e2224..a22f5c4d0 100644 --- a/src/clua-cartesi.cpp +++ b/src/clua-cartesi.cpp @@ -22,6 +22,7 @@ #include "clua.h" #include "machine-c-api.h" #include "riscv-constants.h" +#include "rtc.h" /// \file /// \brief Scripting interface for the Cartesi SDK. @@ -122,6 +123,9 @@ CM_API int luaopen_cartesi(lua_State *L) { clua_setintegerfield(L, CM_UARCH_BREAK_REASON_REACHED_TARGET_CYCLE, "UARCH_BREAK_REASON_REACHED_TARGET_CYCLE", -1); clua_setintegerfield(L, CM_UARCH_BREAK_REASON_HALTED, "UARCH_BREAK_REASON_HALTED", -1); + clua_setintegerfield(L, UINT64_MAX, "MAX_MCYCLE", -1); + clua_setintegerfield(L, RTC_FREQ_DIV, "RTC_FREQ_DIV", -1); + clua_setintegerfield(L, RTC_CLOCK_FREQ, "RTC_CLOCK_FREQ", -1); clua_setintegerfield(L, MVENDORID_INIT, "MVENDORID", -1); clua_setintegerfield(L, MARCHID_INIT, "MARCHID", -1); clua_setintegerfield(L, MIMPID_INIT, "MIMPID", -1); diff --git a/src/machine.cpp b/src/machine.cpp index 1bcd6efd6..dbdec27a8 100644 --- a/src/machine.cpp +++ b/src/machine.cpp @@ -1569,7 +1569,6 @@ void machine::dump_pmas(void) const { } std::array filename{}; (void) sprintf(filename.data(), "%016" PRIx64 "--%016" PRIx64 ".bin", pma->get_start(), pma->get_length()); - std::cerr << "writing to " << filename.data() << '\n'; auto fp = unique_fopen(filename.data(), "wb"); for (uint64_t page_start_in_range = 0; page_start_in_range < pma->get_length(); page_start_in_range += PMA_PAGE_SIZE) { diff --git a/src/spec/all-tests.lua b/src/spec/all-tests.lua new file mode 100644 index 000000000..179419824 --- /dev/null +++ b/src/spec/all-tests.lua @@ -0,0 +1,32 @@ +#!/usr/bin/env lua5.4 + +-- Copyright 2023 Cartesi Pte. Ltd. +-- +-- This file is part of the machine-emulator. The machine-emulator is free +-- software: you can redistribute it and/or modify it under the terms of the GNU +-- Lesser General Public License as published by the Free Software Foundation, +-- either version 3 of the License, or (at your option) any later version. +-- +-- The machine-emulator is distributed in the hope that it will be useful, but +-- WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +-- FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License +-- for more details. +-- +-- You should have received a copy of the GNU Lesser General Public License +-- along with the machine-emulator. If not, see http://www.gnu.org/licenses/. +-- + +local lester = require("spec.util.lester") + +-- Parse arguments from command line. +lester.parse_args() + +require("spec.keccak-tests") +require("spec.config-tests") +require("spec.htif-tests") +require("spec.machine-tests") +require("spec.clua-tests") +require("spec.step-tests") + +lester.report() -- Print overall statistic of the tests run. +lester.exit() -- Exit with success if all tests passed. diff --git a/src/spec/clua-tests.lua b/src/spec/clua-tests.lua new file mode 100644 index 000000000..fcc86c0b0 --- /dev/null +++ b/src/spec/clua-tests.lua @@ -0,0 +1,44 @@ +#!/usr/bin/env lua5.4 + +-- Copyright 2023 Cartesi Pte. Ltd. +-- +-- This file is part of the machine-emulator. The machine-emulator is free +-- software: you can redistribute it and/or modify it under the terms of the GNU +-- Lesser General Public License as published by the Free Software Foundation, +-- either version 3 of the License, or (at your option) any later version. +-- +-- The machine-emulator is distributed in the hope that it will be useful, but +-- WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +-- FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License +-- for more details. +-- +-- You should have received a copy of the GNU Lesser General Public License +-- along with the machine-emulator. If not, see http://www.gnu.org/licenses/. +-- + +local lester = require("spec.util.lester") +local fs = require("spec.util.fs") +local cartesi = require("cartesi") +local describe, it, expect = lester.describe, lester.it, lester.expect + +describe("machine clua", function() + local dummy_machine = cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + }) + + it("should fail when trying to pass non machine to a a machine API", function() + local read_mcycle = dummy_machine.read_mcycle + expect.fail(function() read_mcycle(1) end, "expected cartesi machine object") + expect.fail(function() read_mcycle(nil) end, "expected cartesi machine object") + expect.fail(function() read_mcycle() end, "expected cartesi machine object") + expect.fail(function() read_mcycle({}) end, "expected cartesi machine object") + expect.fail(function() read_mcycle(setmetatable({}, {})) end, "expected cartesi machine object") + end) + + it("should be able to convert a machine to a string", function() + local s = tostring(dummy_machine) + expect.truthy(s) + expect.equal(s:match("[a-z ]+"), "cartesi machine object") + end) +end) diff --git a/src/spec/config-tests.lua b/src/spec/config-tests.lua new file mode 100644 index 000000000..58dbe46a5 --- /dev/null +++ b/src/spec/config-tests.lua @@ -0,0 +1,1120 @@ +#!/usr/bin/env lua5.4 + +-- Copyright 2023 Cartesi Pte. Ltd. +-- +-- This file is part of the machine-emulator. The machine-emulator is free +-- software: you can redistribute it and/or modify it under the terms of the GNU +-- Lesser General Public License as published by the Free Software Foundation, +-- either version 3 of the License, or (at your option) any later version. +-- +-- The machine-emulator is distributed in the hope that it will be useful, but +-- WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +-- FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License +-- for more details. +-- +-- You should have received a copy of the GNU Lesser General Public License +-- along with the machine-emulator. If not, see http://www.gnu.org/licenses/. +-- + +local lester = require("spec.util.lester") +local fs = require("spec.util.fs") +local cartesi = require("cartesi") +local util = require("cartesi.util") +local describe, it, expect = lester.describe, lester.it, lester.expect + +local default_initial_config = cartesi.machine.get_default_config() + +local expected_initial_config = { + processor = { + -- these are non zero and depends in our implementation + marchid = default_initial_config.processor.marchid, + mimpid = default_initial_config.processor.mimpid, + mvendorid = default_initial_config.processor.mvendorid, + misa = default_initial_config.processor.misa, + mstatus = default_initial_config.processor.mstatus, + pc = default_initial_config.processor.pc, + iflags = default_initial_config.processor.iflags, + ilrsc = default_initial_config.processor.ilrsc, + -- these we know in advance + fcsr = 0, + icycleinstret = 0, + mcause = 0, + mcounteren = 0, + mcycle = 0, + medeleg = 0, + menvcfg = 0, + mepc = 0, + mideleg = 0, + mie = 0, + mip = 0, + mscratch = 0, + mtval = 0, + mtvec = 0, + satp = 0, + scause = 0, + scounteren = 0, + senvcfg = 0, + sepc = 0, + sscratch = 0, + stval = 0, + stvec = 0, + x = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + f = { [0] = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + }, + ram = { + image_filename = "", + length = 0x4000, + }, + rom = { + bootargs = "", + image_filename = fs.rom_image, + }, + tlb = { + image_filename = "", + }, + flash_drive = {}, + htif = { + console_getchar = false, + yield_automatic = false, + yield_manual = false, + fromhost = 0, + tohost = 0, + }, + clint = { + mtimecmp = 0, + }, + uarch = { + processor = { + cycle = 0, + pc = default_initial_config.uarch.processor.pc, + x = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, + }, + ram = { + image_filename = "", + length = 0, + }, + }, +} + +local test_config = { + processor = { + -- these are hardwired constants, and cannot change + marchid = default_initial_config.processor.marchid, + mimpid = default_initial_config.processor.mimpid, + mvendorid = default_initial_config.processor.mvendorid, + -- these can be changed, and are set to random values + pc = 0x070c1efa257e32e4, + misa = 0xff13504ee4da72f1, + fcsr = 0x8b337085bc73d6f6, + icycleinstret = 0xf4310770998bfaab, + ilrsc = 0x5bf71f0fc1c516e1, + mcause = 0x73b2dcca2277c070, + mcounteren = 0x2aeb5bbda1f4be71, + mcycle = 0x072a8a6e298b61cb, + medeleg = 0x3d00a03901459100, + menvcfg = 0x4cf38ec0407ba557, + mepc = 0x23aab25abacae88d, + mideleg = 0x2830ed05187f8ab9, + mie = 0x4b615ac9c32e2a91, + mip = 0x4abb22a9f342d65c, + mscratch = 0xa0d39fc9763cdd91, + mstatus = 0xd02e272900ea57d5, + mtval = 0x41cea506fd53c830, + mtvec = 0xa395b0c3b234bfbc, + satp = 0x2fd3e21cd171c484, + scause = 0xc41a4593c61098ca, + scounteren = 0x9fdf00eae96a888d, + senvcfg = 0xe8c06242796cffa3, + sepc = 0x0e1151b658feb88a, + sscratch = 0x59951bc8a8fb4921, + stval = 0xb1c067c2c1709a51, + stvec = 0xa9ca605ecb0807b6, + iflags = 3, + x = { + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + }, + f = { + [0] = 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + }, + }, + ram = { + image_filename = "", + length = fs.get_file_length(fs.linux_image), + }, + rom = { + image_filename = fs.rom_image, + bootargs = "test", + }, + flash_drive = { + { + image_filename = fs.rootfs_image, + length = fs.get_file_length(fs.rootfs_image), + start = 0x80000000000000, + shared = false, + }, + { + image_filename = "", + length = 0x4000, + start = 0x90000000000000, + shared = false, + }, + }, + uarch = { + processor = { + cycle = 7, + pc = 0x2000, + x = { + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + }, + }, + ram = { image_filename = fs.uarch_ram_image, length = 0x20000 }, + }, + htif = { + yield_automatic = true, + yield_manual = true, + console_getchar = false, + fromhost = 0x5555555555555555, + tohost = 0xaaaaaaaaaaaaaaaa, + }, + rollup = { + rx_buffer = { image_filename = "", start = 0x60000000, length = 0x1000, shared = false }, + tx_buffer = { image_filename = "", start = 0x60002000, length = 0x1000, shared = false }, + input_metadata = { image_filename = "", start = 0x60004000, length = 0x1000, shared = false }, + voucher_hashes = { image_filename = "", start = 0x60006000, length = 0x1000, shared = false }, + notice_hashes = { image_filename = "", start = 0x60008000, length = 0x1000, shared = false }, + }, + tlb = { image_filename = "" }, + clint = { mtimecmp = 8192 }, +} + +describe("machine config", function() + it("should set initial configs correctly", function() + do + local machine = cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + }, { + concurrency = { update_merkle_tree = 1 }, + }) + expect.equal(machine:get_initial_config(), expected_initial_config) + machine:destroy() + end + collectgarbage() + end) + + it("should set initial configs correctly for -1 values", function() + local machine = cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + processor = { + marchid = -1, + mvendorid = -1, + mimpid = -1, + }, + }, { + concurrency = { update_merkle_tree = 1 }, + }) + expect.equal(machine:get_initial_config(), expected_initial_config) + end) + + it("should set missing config fields correctly", function() + local config = { + processor = { + x = {}, + f = {}, + }, + ram = { + length = 0x4000, + }, + rom = { + image_filename = fs.rom_image, + }, + flash_drive = { { + length = 0x4000, + start = 0x80000000000000, + } }, + uarch = { + processor = { + x = {}, + }, + rom = { length = 0 }, + ram = { length = 0 }, + }, + rollup = { + rx_buffer = { start = 0x60000000, length = 0x2000 }, + tx_buffer = { start = 0x60002000, length = 0x2000 }, + input_metadata = { start = 0x60004000, length = 0x2000 }, + voucher_hashes = { start = 0x60006000, length = 0x2000 }, + notice_hashes = { start = 0x60008000, length = 0x2000 }, + }, + tlb = {}, + clint = {}, + htif = {}, + } + local expected_machine_config = { + processor = expected_initial_config.processor, + ram = expected_initial_config.ram, + rom = expected_initial_config.rom, + flash_drive = { + { + length = 0x4000, + start = 0x80000000000000, + image_filename = "", + shared = false, + }, + }, + uarch = expected_initial_config.uarch, + rollup = { + rx_buffer = { start = 0x60000000, length = 0x2000, image_filename = "", shared = false }, + tx_buffer = { start = 0x60002000, length = 0x2000, image_filename = "", shared = false }, + input_metadata = { start = 0x60004000, length = 0x2000, image_filename = "", shared = false }, + voucher_hashes = { start = 0x60006000, length = 0x2000, image_filename = "", shared = false }, + notice_hashes = { start = 0x60008000, length = 0x2000, image_filename = "", shared = false }, + }, + tlb = expected_initial_config.tlb, + clint = expected_initial_config.clint, + htif = expected_initial_config.htif, + } + local machine = cartesi.machine(config, {}) + expect.equal(machine:get_initial_config(), expected_machine_config) + end) + + it("should match with initial config", function() + local machine = cartesi.machine(test_config) + expect.equal(machine:get_initial_config(), test_config) + end) + + it("should match halt flags, yield flags and config", function() + local machine = cartesi.machine({ + ram = { length = 0x100000 }, + rom = { image_filename = fs.rom_image }, + }) + -- Get machine default config and test for known fields + local initial_config = machine:get_initial_config() + expect.equal(initial_config.processor.marchid, default_initial_config.processor.marchid) + expect.equal(initial_config.processor.pc, default_initial_config.processor.pc) + expect.equal(initial_config.ram.length, 0x100000) + expect.not_equal(initial_config.rom.image_filename, "") + -- Check machine is not halted + expect.falsy(machine:read_iflags_H()) + -- Check machine is not yielded + expect.falsy(machine:read_iflags_Y() or machine:read_iflags_X()) + end) + + it("should fail when attempting to create machine with invalid configs", function() + -- rom + expect.fail( + function() + cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = "some/invalid/image.bin" }, + }) + end, + "error opening image file" + ) + expect.fail( + function() + cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.linux_image }, + }) + end, + "is too large for range" + ) + + -- ram + expect.fail( + function() + cartesi.machine({ + ram = { image_filename = "some/invalid/image.bin", length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + }) + end, + "error opening image file" + ) + expect.fail( + function() + cartesi.machine({ + ram = { image_filename = fs.linux_image, length = 0 }, + rom = { image_filename = fs.rom_image }, + }) + end, + "length cannot be zero" + ) + expect.fail( + function() + cartesi.machine({ + ram = { image_filename = fs.linux_image, length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + }) + end, + "too large for range" + ) + expect.fail( + function() + cartesi.machine({ + ram = {}, + rom = { image_filename = fs.rom_image }, + }) + end, + "invalid length" + ) + expect.fail( + function() + cartesi.machine({ + ram = { length = 0 }, + rom = { image_filename = fs.rom_image }, + }) + end, + "length cannot be zero" + ) + expect.fail( + function() + cartesi.machine({ + ram = { length = 4095 }, + rom = { image_filename = fs.rom_image }, + }) + end, + "must be multiple of page size" + ) + expect.fail( + function() + cartesi.machine({ + ram = { image_filename = true, length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + }) + end, + "invalid image_filename" + ) + expect.fail( + function() + cartesi.machine({ + ram = 0, + rom = { image_filename = fs.rom_image }, + }) + end, + "missing ram" + ) + + -- processor + expect.fail( + function() + cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + processor = 0, + }) + end, + "missing processor" + ) + expect.fail( + function() + cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + processor = { pc = true }, + }) + end, + "invalid pc" + ) + expect.fail( + function() + cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + processor = { x = { true } }, + }) + end, + "invalid entry" + ) + expect.fail( + function() + cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + processor = { marchid = 0 }, + }) + end, + "marchid mismatch" + ) + expect.fail( + function() + cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + processor = { mimpid = 0 }, + }) + end, + "mimpid mismatch" + ) + expect.fail( + function() + cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + processor = { mvendorid = 0 }, + }) + end, + "mvendorid mismatch" + ) + expect.fail( + function() + cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + processor = { x = 0 }, + }) + end, + "invalid processor.x" + ) + expect.fail( + function() + cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + processor = { f = 0 }, + }) + end, + "invalid processor.f" + ) + + -- uarch.processor + expect.fail( + function() + cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + uarch = { + processor = { x = 0 }, + ram = { length = 0x4000 }, + rom = { length = 0x4000 }, + }, + }) + end, + "invalid uarch.processor.x" + ) + + -- flash drive + expect.fail( + function() + cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + flash_drive = { {}, {}, {}, {}, {}, {}, {}, {}, {} }, + }) + end, + "too many flash drives" + ) + expect.fail( + function() + cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + flash_drive = { false }, + }) + end, + "memory range not a table" + ) + expect.fail( + function() + cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + flash_drive = { {} }, + }) + end, + "invalid start" + ) + expect.fail( + function() + cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + flash_drive = { { start = 0x80000000000000, length = 0 } }, + }) + end, + "length cannot be zero" + ) + expect.fail( + function() + cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + flash_drive = { { start = 0x100000000000000, length = 0x4000 } }, + }) + end, + "must use at most 56 bits to be addressable" + ) + expect.fail( + function() + cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + flash_drive = { { start = 0, length = 0x4000 } }, + }) + end, + "overlaps with range of existing" + ) + expect.fail( + function() + cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + flash_drive = { { start = 0, length = 4095 } }, + }) + end, + "must be multiple of page size" + ) + expect.fail( + function() + cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + flash_drive = { + { + image_filename = "some/invalid/image.bin", + start = 0x80000000000000, + length = 0x4000, + }, + }, + }) + end, + "could not open image file" + ) + expect.fail( + function() + cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + flash_drive = { + { + image_filename = "some/invalid/image.bin", + start = 0x80000000000000, + }, + }, + }) + end, + "unable to obtain length of image file" + ) + expect.fail( + function() + cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + flash_drive = { + { + image_filename = fs.rootfs_image, + start = 0x80000000000000, + length = 0, + }, + }, + }) + end, + "length cannot be zero" + ) + expect.fail( + function() + cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + flash_drive = { + { + image_filename = fs.rootfs_image, + start = 0x80000000000000, + length = 0x4000, + }, + }, + }) + end, + "does not match range length" + ) + + -- rollup + expect.fail( + function() + cartesi.machine({ + ram = { length = 0x4000 }, + rom = { image_filename = fs.rom_image }, + rollup = { + rx_buffer = { start = 0x60000000, length = 0x2000 }, + tx_buffer = { start = 0x60002000, length = 0x2000 }, + input_metadata = { start = 0x60004000, length = 0x2000 }, + voucher_hashes = { start = 0x60006000, length = 0x2000 }, + notice_hashes = { start = 0x60008000, length = 0 }, + }, + }) + end, + "incomplete rollup configuration" + ) + + -- stored + expect.fail(function() cartesi.machine("some/invalid/machine") end, "unable to open") + end) +end) + +describe("machine state", function() + local machine = cartesi.machine(test_config) + local P = 0xf6b75ab2bc471c7 -- random prime used to test register write + + it("should read CSRs", function() + -- check read_... + expect.equal(machine:read_marchid(), test_config.processor.marchid) + expect.equal(machine:read_mimpid(), test_config.processor.mimpid) + expect.equal(machine:read_mvendorid(), test_config.processor.mvendorid) + expect.equal(machine:read_pc(), test_config.processor.pc) + expect.equal(machine:read_misa(), test_config.processor.misa) + expect.equal(machine:read_fcsr(), test_config.processor.fcsr) + expect.equal(machine:read_icycleinstret(), test_config.processor.icycleinstret) + expect.equal(machine:read_ilrsc(), test_config.processor.ilrsc) + expect.equal(machine:read_mcause(), test_config.processor.mcause) + expect.equal(machine:read_mcounteren(), test_config.processor.mcounteren) + expect.equal(machine:read_mcycle(), test_config.processor.mcycle) + expect.equal(machine:read_medeleg(), test_config.processor.medeleg) + expect.equal(machine:read_menvcfg(), test_config.processor.menvcfg) + expect.equal(machine:read_mepc(), test_config.processor.mepc) + expect.equal(machine:read_mideleg(), test_config.processor.mideleg) + expect.equal(machine:read_mie(), test_config.processor.mie) + expect.equal(machine:read_mip(), test_config.processor.mip) + expect.equal(machine:read_mscratch(), test_config.processor.mscratch) + expect.equal(machine:read_mstatus(), test_config.processor.mstatus) + expect.equal(machine:read_mtval(), test_config.processor.mtval) + expect.equal(machine:read_mtvec(), test_config.processor.mtvec) + expect.equal(machine:read_satp(), test_config.processor.satp) + expect.equal(machine:read_scause(), test_config.processor.scause) + expect.equal(machine:read_scounteren(), test_config.processor.scounteren) + expect.equal(machine:read_senvcfg(), test_config.processor.senvcfg) + expect.equal(machine:read_sepc(), test_config.processor.sepc) + expect.equal(machine:read_sscratch(), test_config.processor.sscratch) + expect.equal(machine:read_stval(), test_config.processor.stval) + expect.equal(machine:read_stvec(), test_config.processor.stvec) + expect.equal(machine:read_iflags(), test_config.processor.iflags) + expect.equal(machine:read_uarch_cycle(), test_config.uarch.processor.cycle) + expect.equal(machine:read_uarch_pc(), test_config.uarch.processor.pc) + expect.equal(machine:read_uarch_ram_length(), test_config.uarch.ram.length) + + -- check read_csr + expect.equal(machine:read_csr("marchid"), test_config.processor.marchid) + expect.equal(machine:read_csr("mimpid"), test_config.processor.mimpid) + expect.equal(machine:read_csr("mvendorid"), test_config.processor.mvendorid) + expect.equal(machine:read_csr("pc"), test_config.processor.pc) + expect.equal(machine:read_csr("misa"), test_config.processor.misa) + expect.equal(machine:read_csr("fcsr"), test_config.processor.fcsr) + expect.equal(machine:read_csr("icycleinstret"), test_config.processor.icycleinstret) + expect.equal(machine:read_csr("ilrsc"), test_config.processor.ilrsc) + expect.equal(machine:read_csr("mcause"), test_config.processor.mcause) + expect.equal(machine:read_csr("mcounteren"), test_config.processor.mcounteren) + expect.equal(machine:read_csr("mcycle"), test_config.processor.mcycle) + expect.equal(machine:read_csr("medeleg"), test_config.processor.medeleg) + expect.equal(machine:read_csr("menvcfg"), test_config.processor.menvcfg) + expect.equal(machine:read_csr("mepc"), test_config.processor.mepc) + expect.equal(machine:read_csr("mideleg"), test_config.processor.mideleg) + expect.equal(machine:read_csr("mie"), test_config.processor.mie) + expect.equal(machine:read_csr("mip"), test_config.processor.mip) + expect.equal(machine:read_csr("mscratch"), test_config.processor.mscratch) + expect.equal(machine:read_csr("mstatus"), test_config.processor.mstatus) + expect.equal(machine:read_csr("mtval"), test_config.processor.mtval) + expect.equal(machine:read_csr("mtvec"), test_config.processor.mtvec) + expect.equal(machine:read_csr("satp"), test_config.processor.satp) + expect.equal(machine:read_csr("scause"), test_config.processor.scause) + expect.equal(machine:read_csr("scounteren"), test_config.processor.scounteren) + expect.equal(machine:read_csr("senvcfg"), test_config.processor.senvcfg) + expect.equal(machine:read_csr("sepc"), test_config.processor.sepc) + expect.equal(machine:read_csr("sscratch"), test_config.processor.sscratch) + expect.equal(machine:read_csr("stval"), test_config.processor.stval) + expect.equal(machine:read_csr("stvec"), test_config.processor.stvec) + expect.equal(machine:read_csr("iflags"), test_config.processor.iflags) + expect.equal(machine:read_csr("uarch_cycle"), test_config.uarch.processor.cycle) + expect.equal(machine:read_csr("uarch_pc"), test_config.uarch.processor.pc) + expect.equal(machine:read_csr("uarch_ram_length"), test_config.uarch.ram.length) + + -- check if CSR addresses are valid + local get_csr_addr = cartesi.machine.get_csr_address + expect.equal(machine:read_word(get_csr_addr("marchid")), test_config.processor.marchid) + expect.equal(machine:read_word(get_csr_addr("mimpid")), test_config.processor.mimpid) + expect.equal(machine:read_word(get_csr_addr("mvendorid")), test_config.processor.mvendorid) + expect.equal(machine:read_word(get_csr_addr("pc")), test_config.processor.pc) + expect.equal(machine:read_word(get_csr_addr("misa")), test_config.processor.misa) + expect.equal(machine:read_word(get_csr_addr("fcsr")), test_config.processor.fcsr) + expect.equal(machine:read_word(get_csr_addr("icycleinstret")), test_config.processor.icycleinstret) + expect.equal(machine:read_word(get_csr_addr("ilrsc")), test_config.processor.ilrsc) + expect.equal(machine:read_word(get_csr_addr("mcause")), test_config.processor.mcause) + expect.equal(machine:read_word(get_csr_addr("mcounteren")), test_config.processor.mcounteren) + expect.equal(machine:read_word(get_csr_addr("mcycle")), test_config.processor.mcycle) + expect.equal(machine:read_word(get_csr_addr("medeleg")), test_config.processor.medeleg) + expect.equal(machine:read_word(get_csr_addr("menvcfg")), test_config.processor.menvcfg) + expect.equal(machine:read_word(get_csr_addr("mepc")), test_config.processor.mepc) + expect.equal(machine:read_word(get_csr_addr("mideleg")), test_config.processor.mideleg) + expect.equal(machine:read_word(get_csr_addr("mie")), test_config.processor.mie) + expect.equal(machine:read_word(get_csr_addr("mip")), test_config.processor.mip) + expect.equal(machine:read_word(get_csr_addr("mscratch")), test_config.processor.mscratch) + expect.equal(machine:read_word(get_csr_addr("mstatus")), test_config.processor.mstatus) + expect.equal(machine:read_word(get_csr_addr("mtval")), test_config.processor.mtval) + expect.equal(machine:read_word(get_csr_addr("mtvec")), test_config.processor.mtvec) + expect.equal(machine:read_word(get_csr_addr("satp")), test_config.processor.satp) + expect.equal(machine:read_word(get_csr_addr("scause")), test_config.processor.scause) + expect.equal(machine:read_word(get_csr_addr("scounteren")), test_config.processor.scounteren) + expect.equal(machine:read_word(get_csr_addr("senvcfg")), test_config.processor.senvcfg) + expect.equal(machine:read_word(get_csr_addr("sepc")), test_config.processor.sepc) + expect.equal(machine:read_word(get_csr_addr("sscratch")), test_config.processor.sscratch) + expect.equal(machine:read_word(get_csr_addr("stval")), test_config.processor.stval) + expect.equal(machine:read_word(get_csr_addr("stvec")), test_config.processor.stvec) + expect.equal(machine:read_word(get_csr_addr("iflags")), test_config.processor.iflags) + expect.equal(machine:read_word(get_csr_addr("uarch_cycle")), test_config.uarch.processor.cycle) + expect.equal(machine:read_word(get_csr_addr("uarch_pc")), test_config.uarch.processor.pc) + expect.equal(machine:read_word(get_csr_addr("uarch_ram_length")), test_config.uarch.ram.length) + end) + + it("should write CSRs", function() + local pc = P & ~3 -- make sure it is 4-byte aligned + local a = P + + -- check write_... + expect.equal(machine:write_pc(pc) or machine:read_pc(), pc) + expect.equal(machine:write_misa(a) or machine:read_misa(), a) + expect.equal(machine:write_fcsr(a) or machine:read_fcsr(), a) + expect.equal(machine:write_icycleinstret(a) or machine:read_icycleinstret(), a) + expect.equal(machine:write_ilrsc(a) or machine:read_ilrsc(), a) + expect.equal(machine:write_mcause(a) or machine:read_mcause(), a) + expect.equal(machine:write_mcounteren(a) or machine:read_mcounteren(), a) + expect.equal(machine:write_mcycle(a) or machine:read_mcycle(), a) + expect.equal(machine:write_medeleg(a) or machine:read_medeleg(), a) + expect.equal(machine:write_menvcfg(a) or machine:read_menvcfg(), a) + expect.equal(machine:write_mepc(a) or machine:read_mepc(), a) + expect.equal(machine:write_mideleg(a) or machine:read_mideleg(), a) + expect.equal(machine:write_mie(a) or machine:read_mie(), a) + expect.equal(machine:write_mip(a) or machine:read_mip(), a) + expect.equal(machine:write_mscratch(a) or machine:read_mscratch(), a) + expect.equal(machine:write_mstatus(a) or machine:read_mstatus(), a) + expect.equal(machine:write_mtval(a) or machine:read_mtval(), a) + expect.equal(machine:write_mtvec(a) or machine:read_mtvec(), a) + expect.equal(machine:write_satp(a) or machine:read_satp(), a) + expect.equal(machine:write_scause(a) or machine:read_scause(), a) + expect.equal(machine:write_scounteren(a) or machine:read_scounteren(), a) + expect.equal(machine:write_senvcfg(a) or machine:read_senvcfg(), a) + expect.equal(machine:write_sepc(a) or machine:read_sepc(), a) + expect.equal(machine:write_sscratch(a) or machine:read_sscratch(), a) + expect.equal(machine:write_stval(a) or machine:read_stval(), a) + expect.equal(machine:write_stvec(a) or machine:read_stvec(), a) + expect.equal(machine:write_uarch_cycle(a) or machine:read_uarch_cycle(), a) + expect.equal(machine:write_uarch_pc(pc) or machine:read_uarch_pc(), pc) + expect.equal(machine:write_iflags(0) or machine:read_iflags(), 0) + + -- update values for next writes + pc = pc + 4 + a = ~a + + -- check write_csr + expect.equal(machine:write_csr("pc", pc) or machine:read_pc(), pc) + expect.equal(machine:write_csr("misa", a) or machine:read_misa(), a) + expect.equal(machine:write_csr("fcsr", a) or machine:read_fcsr(), a) + expect.equal(machine:write_csr("icycleinstret", a) or machine:read_icycleinstret(), a) + expect.equal(machine:write_csr("ilrsc", a) or machine:read_ilrsc(), a) + expect.equal(machine:write_csr("mcause", a) or machine:read_mcause(), a) + expect.equal(machine:write_csr("mcounteren", a) or machine:read_mcounteren(), a) + expect.equal(machine:write_csr("mcycle", a) or machine:read_mcycle(), a) + expect.equal(machine:write_csr("medeleg", a) or machine:read_medeleg(), a) + expect.equal(machine:write_csr("menvcfg", a) or machine:read_menvcfg(), a) + expect.equal(machine:write_csr("mepc", a) or machine:read_mepc(), a) + expect.equal(machine:write_csr("mideleg", a) or machine:read_mideleg(), a) + expect.equal(machine:write_csr("mie", a) or machine:read_mie(), a) + expect.equal(machine:write_csr("mip", a) or machine:read_mip(), a) + expect.equal(machine:write_csr("mscratch", a) or machine:read_mscratch(), a) + expect.equal(machine:write_csr("mstatus", a) or machine:read_mstatus(), a) + expect.equal(machine:write_csr("mtval", a) or machine:read_mtval(), a) + expect.equal(machine:write_csr("mtvec", a) or machine:read_mtvec(), a) + expect.equal(machine:write_csr("satp", a) or machine:read_satp(), a) + expect.equal(machine:write_csr("scause", a) or machine:read_scause(), a) + expect.equal(machine:write_csr("scounteren", a) or machine:read_scounteren(), a) + expect.equal(machine:write_csr("senvcfg", a) or machine:read_senvcfg(), a) + expect.equal(machine:write_csr("sepc", a) or machine:read_sepc(), a) + expect.equal(machine:write_csr("sscratch", a) or machine:read_sscratch(), a) + expect.equal(machine:write_csr("stval", a) or machine:read_stval(), a) + expect.equal(machine:write_csr("stvec", a) or machine:read_stvec(), a) + expect.equal(machine:write_csr("uarch_cycle", a) or machine:read_uarch_cycle(), a) + expect.equal(machine:write_csr("uarch_pc", pc) or machine:read_uarch_pc(), pc) + expect.equal(machine:write_csr("iflags", 0) or machine:read_iflags(), 0) + end) + + it("should read/set/reset iflags", function() + expect.equal(machine:read_iflags_H(), false) + expect.equal(machine:read_iflags_X(), false) + expect.equal(machine:read_iflags_Y(), false) + expect.equal(machine:set_iflags_H() or machine:read_iflags_H(), true) + expect.equal(machine:set_iflags_X() or machine:read_iflags_X(), true) + expect.equal(machine:set_iflags_Y() or machine:read_iflags_Y(), true) + expect.equal(machine:reset_iflags_X() or machine:read_iflags_X(), false) + expect.equal(machine:reset_iflags_Y() or machine:read_iflags_Y(), false) + end) + + it("should read/write x registers", function() + expect.equal(machine:read_x(0), 0) + for i, defval in ipairs(test_config.processor.x) do + local addr = cartesi.machine.get_x_address(i) + local val = i * P + expect.equal(machine:read_x(i), defval) + expect.equal(machine:read_word(addr), defval) + expect.equal(machine:write_x(i, val) or machine:read_x(i), val) + expect.equal(machine:read_word(addr), val) + end + end) + + it("should read/write f registers", function() + for i = 0, 31 do + local addr = cartesi.machine.get_f_address(i) + local defval = test_config.processor.f[i] + local val = (i + 1) * P + expect.equal(machine:read_f(i), defval) + expect.equal(machine:read_word(addr), defval) + expect.equal(machine:write_f(i, val) or machine:read_f(i), val) + expect.equal(machine:read_word(addr), val) + end + end) + + it("should read/write uarch x registers", function() + expect.equal(machine:read_uarch_x(0), 0) + for i, defval in ipairs(test_config.uarch.processor.x) do + local val = i * P + expect.equal(machine:read_uarch_x(i), defval) + expect.equal(machine:write_uarch_x(i, val) or machine:read_uarch_x(i), val) + end + end) + + it("should read/write htif device", function() + expect.equal(machine:read_htif_fromhost(), test_config.htif.fromhost) + expect.equal(machine:read_csr("htif_fromhost"), test_config.htif.fromhost) + expect.equal(machine:write_htif_fromhost(P) or machine:read_htif_fromhost(), P) + expect.equal(machine:write_htif_fromhost_data(0) or machine:read_htif_fromhost(), P & ~0xffffffffffff) + expect.equal(machine:write_csr("htif_fromhost", ~P) or machine:read_htif_fromhost(), ~P) + + expect.equal(machine:read_htif_tohost(), test_config.htif.tohost) + expect.equal(machine:read_htif_tohost_data(), test_config.htif.tohost & 0xffffffffffff) + expect.equal(machine:read_htif_tohost_cmd(), (test_config.htif.tohost >> 48) & 0xff) + expect.equal(machine:read_htif_tohost_dev(), (test_config.htif.tohost >> 56) & 0xff) + expect.equal(machine:read_csr("htif_tohost"), test_config.htif.tohost) + expect.equal(machine:write_htif_tohost(P) or machine:read_htif_tohost(), P) + expect.equal(machine:write_csr("htif_tohost", ~P) or machine:read_htif_tohost(), ~P) + + expect.equal(machine:read_htif_ihalt(), 0x1) + expect.equal(machine:read_csr("htif_ihalt"), 0x1) + -- expect.equal(machine:write_htif_ihalt(P) or machine:read_htif_ihalt(), P) -- missing method? + expect.equal(machine:write_csr("htif_ihalt", ~P) or machine:read_htif_ihalt(), ~P) + + expect.equal(machine:read_htif_iyield(), 0x3) + expect.equal(machine:read_csr("htif_iyield"), 0x3) + -- expect.equal(machine:write_htif_iyield(P) or machine:read_htif_iyield(), P) -- missing method? + expect.equal(machine:write_csr("htif_iyield", ~P) or machine:read_htif_iyield(), ~P) + + expect.equal(machine:read_htif_iconsole(), 0x2) + expect.equal(machine:read_csr("htif_iconsole"), 0x2) + -- expect.equal(machine:write_htif_iconsole(P) or machine:read_htif_iconsole(), P) -- missing method? + expect.equal(machine:write_csr("htif_iconsole", ~P) or machine:read_htif_iconsole(), ~P) + end) + + it("should read/write clint device", function() + expect.equal(machine:read_clint_mtimecmp(), test_config.clint.mtimecmp) + expect.equal(machine:read_csr("clint_mtimecmp"), test_config.clint.mtimecmp) + expect.equal(machine:write_clint_mtimecmp(P) or machine:read_clint_mtimecmp(), P) + expect.equal(machine:write_csr("clint_mtimecmp", ~P) or machine:read_clint_mtimecmp(), ~P) + end) + + it("should fail when attempting to perform invalid writes", function() + expect.fail(function() machine:write_csr("unknown_csr", 0) end, "unknown csr") + expect.fail(function() machine:write_csr("marchid", 0) end, "is read-only") + expect.fail(function() machine:write_csr("mimpid", 0) end, "is read-only") + expect.fail(function() machine:write_csr("mvendorid", 0) end, "is read-only") + expect.fail(function() machine:write_csr("uarch_ram_length", 0) end, "is read-only") + expect.fail(function() machine:write_pc() end, "got no value") + expect.fail(function() machine:write_x(1) end, "got no value") + expect.fail(function() machine:write_x(1, nil) end, "got nil") + expect.fail(function() machine:write_x(nil, 1) end, "got nil") + expect.fail(function() machine:write_x(1, false) end, "got boolean") + expect.fail(function() machine:write_x(0, 0) end, "register index out of range") + expect.fail(function() machine:write_x(32, 0) end, "register index out of range") + expect.fail(function() machine:write_f(-1, 0) end, "register index out of range") + expect.fail(function() machine:write_f(32, 0) end, "register index out of range") + expect.fail(function() machine:write_uarch_x(-1, 0) end, "register index out of range") + expect.fail(function() machine:write_uarch_x(0, 0) end, "register index out of range") + end) + + it("should fail when attempting to perform invalid reads", function() + expect.fail(function() machine:read_csr("unknown_csr") end, "unknown csr") + expect.fail(function() machine:read_x(-1, 0) end, "register index out of range") + expect.fail(function() machine:read_x(32, 0) end, "register index out of range") + expect.fail(function() machine:read_f(-1, 0) end, "register index out of range") + expect.fail(function() machine:read_f(32, 0) end, "register index out of range") + expect.fail(function() machine:read_uarch_x(-1, 0) end, "register index out of range") + expect.fail(function() machine:read_uarch_x(32, 0) end, "register index out of range") + end) + + it("it should fail when attempting to get address for invalid registers", function() + expect.fail(function() cartesi.machine.get_csr_address() end, "got no value") + expect.fail(function() cartesi.machine.get_csr_address(false) end, "got boolean") + expect.fail(function() cartesi.machine.get_csr_address("") end, "unknown csr") + expect.fail(function() cartesi.machine.get_csr_address("unknown_csr") end, "unknown csr") + expect.fail(function() cartesi.machine.get_x_address(-1) end, "register index out of range") + expect.fail(function() cartesi.machine.get_x_address(32) end, "register index out of range") + expect.fail(function() cartesi.machine.get_f_address(-1) end, "register index out of range") + expect.fail(function() cartesi.machine.get_f_address(32) end, "register index out of range") + end) + + machine:destroy() +end) + +describe("machine rollback", function() + it("should fail when attempting to perform a snapshot or rollback", function() + local machine = cartesi.machine(test_config) + expect.fail(function() machine:snapshot() end, "snapshot is not supported") + expect.fail(function() machine:rollback() end, "rollback is not supported") + end) +end) + +describe("machine store", function() + local function remove_temporary_files() + fs.remove_files({ + "temp_machine/0000000000001000-f000.bin", + "temp_machine/0000000000020000-6000.bin", + "temp_machine/0000000060000000-1000.bin", + "temp_machine/0000000060002000-1000.bin", + "temp_machine/0000000060004000-1000.bin", + "temp_machine/0000000060006000-1000.bin", + "temp_machine/0000000060008000-1000.bin", + "temp_machine/0000000070000000-20000.bin", + "temp_machine/0000000080000000-ee1000.bin", + "temp_machine/0080000000000000-4400000.bin", + "temp_machine/0090000000000000-4000.bin", + "temp_machine/config.protobuf", + "temp_machine/hash", + "temp_machine", + }) + end + + lester.before(remove_temporary_files) + lester.after(remove_temporary_files) + + it("should match hashes and configs between loaded and stored machines", function() + local saved_machine = cartesi.machine(test_config) + local saved_machine_hash = util.hexhash(saved_machine:get_root_hash()) + local saved_machine_config = saved_machine:get_initial_config() + saved_machine:store("temp_machine") + + local loaded_machine = cartesi.machine("temp_machine") + local loaded_machine_hash = util.hexhash(loaded_machine:get_root_hash()) + local loaded_machine_config = loaded_machine:get_initial_config() + + expect.equal(loaded_machine_hash, saved_machine_hash) + + -- all image filenames are lost and changed when using store + saved_machine_config.flash_drive[1].image_filename = "temp_machine/0080000000000000-4400000.bin" + saved_machine_config.flash_drive[2].image_filename = "temp_machine/0090000000000000-4000.bin" + saved_machine_config.ram.image_filename = "temp_machine/0000000080000000-ee1000.bin" + saved_machine_config.rollup.input_metadata.image_filename = "temp_machine/0000000060004000-1000.bin" + saved_machine_config.rollup.notice_hashes.image_filename = "temp_machine/0000000060008000-1000.bin" + saved_machine_config.rollup.rx_buffer.image_filename = "temp_machine/0000000060000000-1000.bin" + saved_machine_config.rollup.tx_buffer.image_filename = "temp_machine/0000000060002000-1000.bin" + saved_machine_config.rollup.voucher_hashes.image_filename = "temp_machine/0000000060006000-1000.bin" + saved_machine_config.rom.image_filename = "temp_machine/0000000000001000-f000.bin" + saved_machine_config.tlb.image_filename = "temp_machine/0000000000020000-6000.bin" + saved_machine_config.uarch.ram.image_filename = "temp_machine/0000000070000000-20000.bin" + + -- bootargs are lost when using store() + saved_machine_config.rom.bootargs = "" + + expect.equal(loaded_machine_config, saved_machine_config) + end) + + it("should fail when trying to saving into an invalid directory", function() + local machine = cartesi.machine(test_config) + expect.fail(function() machine:store("some/invalid/directory") end, "error creating directory") + end) +end) diff --git a/src/spec/htif-tests.lua b/src/spec/htif-tests.lua new file mode 100644 index 000000000..841ed4179 --- /dev/null +++ b/src/spec/htif-tests.lua @@ -0,0 +1,202 @@ +#!/usr/bin/env lua5.4 + +-- Copyright 2023 Cartesi Pte. Ltd. +-- +-- This file is part of the machine-emulator. The machine-emulator is free +-- software: you can redistribute it and/or modify it under the terms of the GNU +-- Lesser General Public License as published by the Free Software Foundation, +-- either version 3 of the License, or (at your option) any later version. +-- +-- The machine-emulator is distributed in the hope that it will be useful, but +-- WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +-- FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License +-- for more details. +-- +-- You should have received a copy of the GNU Lesser General Public License +-- along with the machine-emulator. If not, see http://www.gnu.org/licenses/. +-- + +local cartesi = require("cartesi") +local fs = require("spec.util.fs") +local lester = require("spec.util.lester") +local has_luaposix, unistd = pcall(require, "posix.unistd") +local describe, it, expect = lester.describe, lester.it, lester.expect + +describe("machine htif", function() + -- This test will fetch the rollup buffers from the PMA entries; check + -- that `rx_buffer` and `input_metadata` are filled with a byte patern; + -- then write a byte pattern into `tx_buffer`, `voucher_hashes` and + -- `notice_hashes`. + it("should write/read rollup buffers", function() + local ROLLUP_BUFFER_LENGTH = 4096 + local machine_config = { + ram = { image_filename = fs.tests_path .. "htif_rollup.bin", length = 0x4000000 }, + rom = { image_filename = fs.tests_path .. "bootstrap.bin" }, + htif = { yield_automatic = true }, + rollup = { + rx_buffer = { start = 0x60000000, length = ROLLUP_BUFFER_LENGTH, shared = false }, + tx_buffer = { start = 0x60001000, length = ROLLUP_BUFFER_LENGTH, shared = false }, + input_metadata = { start = 0x60002000, length = ROLLUP_BUFFER_LENGTH, shared = false }, + voucher_hashes = { start = 0x60003000, length = ROLLUP_BUFFER_LENGTH, shared = false }, + notice_hashes = { start = 0x60004000, length = ROLLUP_BUFFER_LENGTH, shared = false }, + }, + } + local machine = cartesi.machine(machine_config) + -- fill input with `pattern` + local pattern = string.rep("\xef\xcd\xab\x89\x67\x45\x23\x01", ROLLUP_BUFFER_LENGTH / 8) + local rollup = machine_config.rollup + machine:write_memory(rollup.rx_buffer.start, pattern, rollup.rx_buffer.length) + -- fill input_metadata with `pattern` + machine:write_memory(rollup.input_metadata.start, pattern, rollup.input_metadata.length) + machine:run(math.maxinteger) + -- check that buffers got filled in with `pattern` + expect.equal(pattern, machine:read_memory(rollup.tx_buffer.start, rollup.tx_buffer.length)) + expect.equal(pattern, machine:read_memory(rollup.voucher_hashes.start, rollup.voucher_hashes.length)) + expect.equal(pattern, machine:read_memory(rollup.notice_hashes.start, rollup.notice_hashes.length)) + expect.truthy(machine:read_iflags_H()) + expect.equal(machine:read_mcycle(), 8981) + expect.equal(machine:read_htif_tohost_data() >> 1, 0) + end) + + local YIELD_MANUAL = cartesi.machine.HTIF_YIELD_MANUAL + local YIELD_AUTOMATIC = cartesi.machine.HTIF_YIELD_AUTOMATIC + local yields = { + { mcycle = 13, data = 10, cmd = YIELD_MANUAL, reason = cartesi.machine.HTIF_YIELD_REASON_PROGRESS }, + { mcycle = 44, data = 11, cmd = YIELD_MANUAL, reason = cartesi.machine.HTIF_YIELD_REASON_PROGRESS }, + { mcycle = 75, data = 12, cmd = YIELD_MANUAL, reason = cartesi.machine.HTIF_YIELD_REASON_PROGRESS }, + { mcycle = 107, data = 13, cmd = YIELD_MANUAL, reason = cartesi.machine.HTIF_YIELD_REASON_RX_ACCEPTED }, + { mcycle = 139, data = 14, cmd = YIELD_MANUAL, reason = cartesi.machine.HTIF_YIELD_REASON_RX_REJECTED }, + { mcycle = 171, data = 15, cmd = YIELD_MANUAL, reason = cartesi.machine.HTIF_YIELD_REASON_TX_VOUCHER }, + { mcycle = 203, data = 16, cmd = YIELD_MANUAL, reason = cartesi.machine.HTIF_YIELD_REASON_TX_NOTICE }, + { mcycle = 235, data = 17, cmd = YIELD_MANUAL, reason = cartesi.machine.HTIF_YIELD_REASON_TX_REPORT }, + { mcycle = 267, data = 18, cmd = YIELD_MANUAL, reason = cartesi.machine.HTIF_YIELD_REASON_TX_EXCEPTION }, + { mcycle = 298, data = 20, cmd = YIELD_AUTOMATIC, reason = cartesi.machine.HTIF_YIELD_REASON_PROGRESS }, + { mcycle = 329, data = 21, cmd = YIELD_AUTOMATIC, reason = cartesi.machine.HTIF_YIELD_REASON_PROGRESS }, + { mcycle = 360, data = 22, cmd = YIELD_AUTOMATIC, reason = cartesi.machine.HTIF_YIELD_REASON_PROGRESS }, + { mcycle = 392, data = 23, cmd = YIELD_AUTOMATIC, reason = cartesi.machine.HTIF_YIELD_REASON_RX_ACCEPTED }, + { mcycle = 424, data = 24, cmd = YIELD_AUTOMATIC, reason = cartesi.machine.HTIF_YIELD_REASON_RX_REJECTED }, + { mcycle = 456, data = 25, cmd = YIELD_AUTOMATIC, reason = cartesi.machine.HTIF_YIELD_REASON_TX_VOUCHER }, + { mcycle = 488, data = 26, cmd = YIELD_AUTOMATIC, reason = cartesi.machine.HTIF_YIELD_REASON_TX_NOTICE }, + { mcycle = 520, data = 27, cmd = YIELD_AUTOMATIC, reason = cartesi.machine.HTIF_YIELD_REASON_TX_REPORT }, + } + local function make_yield_test(yield_automatic_enable, yield_manual_enable) + local test_name = + string.format("should sink for yield (automatic=%s manual=%s)", yield_automatic_enable, yield_manual_enable) + it(test_name, function() + local machine_config = { + ram = { image_filename = fs.tests_path .. "htif_yield.bin", length = 0x4000000 }, + rom = { image_filename = fs.tests_path .. "bootstrap.bin" }, + htif = { yield_automatic = yield_automatic_enable, yield_manual = yield_manual_enable }, + } + local machine = cartesi.machine(machine_config) + local break_reason + for _, v in ipairs(yields) do + if + (v.cmd == YIELD_MANUAL and yield_manual_enable) + or (v.cmd == YIELD_AUTOMATIC and yield_automatic_enable) + then + while not machine:read_iflags_Y() and not machine:read_iflags_X() and not machine:read_iflags_H() do + break_reason = machine:run() + end + -- mcycle should be as expected + local mcycle = machine:read_mcycle() + expect.equal(mcycle, v.mcycle) + + if yield_automatic_enable and v.cmd == YIELD_AUTOMATIC then + expect.equal(break_reason, cartesi.BREAK_REASON_YIELDED_AUTOMATICALLY) + expect.truthy(machine:read_iflags_X()) + expect.falsy(machine:read_iflags_Y()) + elseif yield_manual_enable and v.cmd == YIELD_MANUAL then + expect.equal(break_reason, cartesi.BREAK_REASON_YIELDED_MANUALLY) + expect.truthy(machine:read_iflags_Y()) + expect.falsy(machine:read_iflags_X()) + else + expect.truthy(false) + end + -- data should be as expected + local data = machine:read_htif_tohost_data() + local reason = data >> 32 + data = data << 32 >> 32 + expect.equal(data, v.data) + expect.equal(reason, v.reason) + expect.equal(machine:read_htif_tohost_cmd(), v.cmd) + -- trying to run it without resetting iflags.Y should not advance + if machine:read_iflags_Y() then + machine:run() + expect.equal(machine:read_mcycle(), mcycle) + expect.truthy(machine:read_iflags_Y()) + end + -- now reset it so the machine can be advanced + machine:reset_iflags_Y() + machine:reset_iflags_X() + end + end + -- finally run to completion + while not machine:read_iflags_Y() and not machine:read_iflags_H() do + break_reason = machine:run() + end + -- should be halted + expect.equal(break_reason, cartesi.BREAK_REASON_HALTED) + expect.truthy(machine:read_iflags_H()) + -- at the expected mcycle + expect.equal(machine:read_mcycle(), 561) + -- with the expected payload + expect.equal((machine:read_htif_tohost_data() >> 1), 42) + end) + end + + make_yield_test(false, false) + make_yield_test(false, true) + make_yield_test(true, false) + make_yield_test(true, true) + + it("should write to console when getchar is disabled", function() + local machine = cartesi.machine({ + ram = { image_filename = fs.tests_path .. "htif_console.bin", length = 0x4000000 }, + rom = { image_filename = fs.tests_path .. "bootstrap.bin" }, + htif = { console_getchar = false }, + }) + machine:run(math.maxinteger) + -- should be halted + expect.truthy(machine:read_iflags_H()) + -- with the expected payload + expect.equal((machine:read_htif_tohost_data() >> 1), 42) + -- at the expected mcycle + expect.equal(machine:read_mcycle(), 2141) + io.write("\n") + end) + + -- This test is only enabled if luaposix is installed in the system + it("should read/write to console when getchar is enabled", function() + -- create new FD for stdin and write in it, + -- later the cartesi machine console will consume this value + local read_fd, write_fd = unistd.pipe() + unistd.dup2(read_fd, unistd.STDIN_FILENO) + unistd.write(write_fd, "CTSI") + local machine = cartesi.machine({ + ram = { image_filename = fs.tests_path .. "htif_console.bin", length = 0x4000000 }, + rom = { image_filename = fs.tests_path .. "bootstrap.bin" }, + htif = { console_getchar = true }, + }) + machine:run(math.maxinteger) + -- should be halted + expect.truthy(machine:read_iflags_H()) + -- with the expected payload + expect.equal((machine:read_htif_tohost_data() >> 1), 42) + -- at the expected mcycle + expect.equal(machine:read_mcycle(), 2141) + io.write("\n") + + -- we cannot initialize TTY twice + expect.fail( + function() + cartesi.machine({ + ram = { image_filename = fs.tests_path .. "htif_console.bin", length = 0x4000000 }, + rom = { image_filename = fs.tests_path .. "bootstrap.bin" }, + htif = { console_getchar = true }, + }) + end, + "TTY already initialized" + ) + end, has_luaposix) +end) diff --git a/src/spec/keccak-tests.lua b/src/spec/keccak-tests.lua new file mode 100644 index 000000000..a7096faee --- /dev/null +++ b/src/spec/keccak-tests.lua @@ -0,0 +1,69 @@ +#!/usr/bin/env lua5.4 + +-- Copyright 2023 Cartesi Pte. Ltd. +-- +-- This file is part of the machine-emulator. The machine-emulator is free +-- software: you can redistribute it and/or modify it under the terms of the GNU +-- Lesser General Public License as published by the Free Software Foundation, +-- either version 3 of the License, or (at your option) any later version. +-- +-- The machine-emulator is distributed in the hope that it will be useful, but +-- WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +-- FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License +-- for more details. +-- +-- You should have received a copy of the GNU Lesser General Public License +-- along with the machine-emulator. If not, see http://www.gnu.org/licenses/. +-- + +local lester = require("spec.util.lester") +local util = require("cartesi.util") +local describe, it, expect = lester.describe, lester.it, lester.expect +local keccak = require("cartesi").keccak + +local function hexkeccak(...) return util.hexhash(keccak(...)) end + +describe("keccak", function() + it("should fail when passing invalid arguments", function() + expect.fail(function() keccak("a", "b", "c") end, "too many arguments") + expect.fail(function() keccak(1, 2) end, "too many arguments") + expect.fail(function() keccak() end, "too few arguments") + end) + + it("should match hashes for uint64 integers", function() + expect.equal(hexkeccak(0), "011b4d03dd8c01f1049143cf9c4c817e4b167f1d1b83e5c6f0f10d89ba1e7bce") + expect.equal(hexkeccak(1), "30f692b256e24009bcb34d0ee84da73c298afacc0924e01105e2eb0f01a87fe2") + expect.equal(hexkeccak(-1), "ad0bfb4b0a66700aeb759d88c315168cc0a11ee99e2a680e548ecf0a464e7daf") + expect.equal(hexkeccak(0x8000000000000000), "f9b31243137c51434c88c419b2a3d7d2103a13948255efab17ca486946dfbf49") + expect.equal(hexkeccak(0xf0f10d89ba1e7bce), "86433232ac2024ad7962ccc2fbb7c0219499b98ec24049a81ca6484d206eb288") + end) + + it("should match hashes for one string", function() + expect.equal(hexkeccak(""), "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470") + expect.equal(hexkeccak("0"), "044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d") + expect.equal(hexkeccak("test"), "9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658") + expect.equal(hexkeccak(hexkeccak("")), "79482f93ea0d714e293366322922962af38ecdd95cff648355c1af4b40a78b32") + end) + + it("should match hashes for two strings", function() + expect.equal(hexkeccak("", ""), "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470") + expect.equal(hexkeccak("0", ""), "044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d") + expect.equal(hexkeccak("", "0"), "044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d") + expect.equal(hexkeccak("test", ""), "9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658") + expect.equal(hexkeccak("tes", "t"), "9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658") + expect.equal(hexkeccak("te", "st"), "9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658") + expect.equal(hexkeccak("t", "est"), "9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658") + expect.equal(hexkeccak("", "test"), "9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658") + end) + + it("should match hashes for large ranges", function() + expect.equal( + hexkeccak(string.rep("a", 8191)), + "b52a6c73f463177a28d89360fb470808ba6572ec75de6db05a3bb044ca4d1009" + ) + expect.equal( + hexkeccak(string.rep("a", 4096), string.rep("a", 4095)), + "b52a6c73f463177a28d89360fb470808ba6572ec75de6db05a3bb044ca4d1009" + ) + end) +end) diff --git a/src/spec/machine-tests.lua b/src/spec/machine-tests.lua new file mode 100644 index 000000000..c23aa986b --- /dev/null +++ b/src/spec/machine-tests.lua @@ -0,0 +1,96 @@ +#!/usr/bin/env lua5.4 + +-- Copyright 2023 Cartesi Pte. Ltd. +-- +-- This file is part of the machine-emulator. The machine-emulator is free +-- software: you can redistribute it and/or modify it under the terms of the GNU +-- Lesser General Public License as published by the Free Software Foundation, +-- either version 3 of the License, or (at your option) any later version. +-- +-- The machine-emulator is distributed in the hope that it will be useful, but +-- WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +-- FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License +-- for more details. +-- +-- You should have received a copy of the GNU Lesser General Public License +-- along with the machine-emulator. If not, see http://www.gnu.org/licenses/. +-- + +local lester = require("spec.util.lester") +local fs = require("spec.util.fs") +local util = require("cartesi.util") +local cartesi = require("cartesi") +local keccak = require("cartesi").keccak +local describe, it, expect = lester.describe, lester.it, lester.expect + +describe("machine run", function() + it("should not break due to mtime interrupts", function() + local machine = cartesi.machine({ + rom = { image_filename = fs.tests_path .. "bootstrap.bin" }, + ram = { image_filename = fs.tests_path .. "mtime_interrupt.bin", length = 1 << 20 }, + }) + machine:run() + expect.truthy(machine:read_iflags_H()) + expect.equal(machine:read_htif_tohost_data() >> 1, 0) + expect.equal(machine:read_mcycle(), cartesi.RTC_FREQ_DIV * 2 + 20) + end) + + it("should run up to mcycle limit", function() + local machine = cartesi.machine({ + rom = { image_filename = fs.tests_path .. "bootstrap.bin" }, + ram = { image_filename = fs.tests_path .. "mcycle_overflow.bin", length = 1 << 20 }, + }) + -- Stop the machine before the first RAM instruction + local WFI_CYCLE = 7 + expect.equal(machine:run(WFI_CYCLE), cartesi.BREAK_REASON_REACHED_TARGET_MCYCLE) + machine:write_mcycle(cartesi.MAX_MCYCLE - 5) + -- Run once to trigger an interrupt, which might cause an overflow on the + -- next call to machine:run + expect.equal(machine:run(cartesi.MAX_MCYCLE - 4), cartesi.BREAK_REASON_REACHED_TARGET_MCYCLE) + expect.equal(machine:run(cartesi.MAX_MCYCLE), cartesi.BREAK_REASON_REACHED_TARGET_MCYCLE) + expect.equal(machine:read_mcycle(), cartesi.MAX_MCYCLE) + end) + + it("shouldn't change state in max mcycle", function() + local machine = cartesi.machine({ + rom = { image_filename = fs.tests_path .. "bootstrap.bin" }, + ram = { length = 1 << 20 }, + }) + machine:write_mcycle(cartesi.MAX_MCYCLE) + local hash_before = machine:get_root_hash() + expect.equal(machine:run(cartesi.MAX_MCYCLE), cartesi.BREAK_REASON_REACHED_TARGET_MCYCLE) + local hash_after = machine:get_root_hash() + expect.equal(hash_before, hash_after) + end) +end) + +describe("machine dump", function() + local pmas_file_names = { + "0000000000000000--0000000000001000.bin", -- shadow state + "0000000000001000--000000000000f000.bin", -- rom + "0000000000010000--0000000000001000.bin", -- shadow pmas + "0000000000020000--0000000000006000.bin", -- shadow tlb + "0000000002000000--00000000000c0000.bin", -- clint + "0000000040008000--0000000000001000.bin", -- htif + "0000000080000000--0000000000100000.bin", -- ram + } + local config = { + rom = { image_filename = fs.rom_image }, + ram = { length = 1 << 20 }, + } + + -- Auto remove PMA bin files after each test + lester.after(function() fs.remove_files(pmas_file_names) end) + + it("should match pmas dumps", function() + local machine = cartesi.machine(config) + machine:dump_pmas() + for _, file_name in ipairs(pmas_file_names) do + local mem_start, mem_size = file_name:match("^(%x+)%-%-(%x+)%.bin$") + mem_start, mem_size = tonumber(mem_start, 16), tonumber(mem_size, 16) + local file_mem = fs.read_file(file_name) + local machine_mem = machine:read_memory(mem_start, mem_size) + expect.equal(util.hexhash(keccak(file_mem)), util.hexhash(keccak(machine_mem))) + end + end) +end) diff --git a/src/spec/step-tests.lua b/src/spec/step-tests.lua new file mode 100644 index 000000000..1c1b8b746 --- /dev/null +++ b/src/spec/step-tests.lua @@ -0,0 +1,58 @@ +#!/usr/bin/env lua5.4 + +-- Copyright 2023 Cartesi Pte. Ltd. +-- +-- This file is part of the machine-emulator. The machine-emulator is free +-- software: you can redistribute it and/or modify it under the terms of the GNU +-- Lesser General Public License as published by the Free Software Foundation, +-- either version 3 of the License, or (at your option) any later version. +-- +-- The machine-emulator is distributed in the hope that it will be useful, but +-- WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +-- FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License +-- for more details. +-- +-- You should have received a copy of the GNU Lesser General Public License +-- along with the machine-emulator. If not, see http://www.gnu.org/licenses/. +-- + +local lester = require("spec.util.lester") +local fs = require("spec.util.fs") +local cartesi = require("cartesi") +local describe, it, expect = lester.describe, lester.it, lester.expect + +describe("machine step_uarch", function() + local only_rom_config = { + ram = { length = 0x4000000 }, + rom = { image_filename = fs.rom_image }, + uarch = { + ram = { image_filename = fs.uarch_ram_image, length = 0x20000 }, + }, + } + it("should verify state transition and access log", function() + local machine = cartesi.machine(only_rom_config) + local old_hash = machine:get_root_hash() + local access_log = machine:step_uarch({ proofs = true, annotations = true }) + expect.truthy(access_log.brackets) + expect.truthy(access_log.accesses) + expect.truthy(access_log.notes) + local new_hash = machine:get_root_hash() + local res = cartesi.machine.verify_state_transition(old_hash, access_log, new_hash, {}) + expect.equal(res, 1) + res = cartesi.machine.verify_access_log(access_log, {}) + expect.equal(res, 1) + end) + + for _, proofs in ipairs({ true, false }) do + it(string.format("should do nothing on max mcycle (proofs=%s)", proofs), function() + local machine = cartesi.machine(only_rom_config) + machine:write_mcycle(cartesi.MAX_MCYCLE) + local log = machine:step_uarch({ proofs = proofs }) + expect.equal(#log.accesses, 7) + local old_hash = machine:get_root_hash() + expect.equal(machine:read_mcycle(), cartesi.MAX_MCYCLE) + local new_hash = machine:get_root_hash() + expect.equal(old_hash, new_hash) + end) + end +end) diff --git a/src/spec/util/fs.lua b/src/spec/util/fs.lua new file mode 100644 index 000000000..966b83fee --- /dev/null +++ b/src/spec/util/fs.lua @@ -0,0 +1,55 @@ +#!/usr/bin/env lua5.4 + +-- Copyright 2023 Cartesi Pte. Ltd. +-- +-- This file is part of the machine-emulator. The machine-emulator is free +-- software: you can redistribute it and/or modify it under the terms of the GNU +-- Lesser General Public License as published by the Free Software Foundation, +-- either version 3 of the License, or (at your option) any later version. +-- +-- The machine-emulator is distributed in the hope that it will be useful, but +-- WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +-- FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License +-- for more details. +-- +-- You should have received a copy of the GNU Lesser General Public License +-- along with the machine-emulator. If not, see http://www.gnu.org/licenses/. +-- + +local fs = {} + +function fs.adjust_images_path(path) + if not path then return "" end + return string.gsub(path, "/*$", "") .. "/" +end + +function fs.remove_files(filenames) + for _, filename in pairs(filenames) do + os.remove(filename) + end +end + +function fs.read_file(filename) + local file = assert(io.open(filename, "rb")) + if not file then return nil end + local contents = file:read("*a") + file:close() + return contents +end + +function fs.get_file_length(filename) + local file = io.open(filename, "rb") + if not file then return nil end + local size = file:seek("end") + file:close() + return size +end + +fs.images_path = fs.adjust_images_path(os.getenv("CARTESI_IMAGES_PATH")) +fs.tests_path = fs.adjust_images_path(os.getenv("CARTESI_TESTS_PATH")) +fs.rom_image = fs.images_path .. "rom.bin" +fs.linux_image = fs.images_path .. "linux.bin" +fs.rootfs_image = fs.images_path .. "rootfs.ext2" +fs.uarch_ram_image = fs.images_path .. "uarch-ram.bin" + +return fs diff --git a/src/spec/util/lester.lua b/src/spec/util/lester.lua new file mode 100644 index 000000000..273bb2e22 --- /dev/null +++ b/src/spec/util/lester.lua @@ -0,0 +1,594 @@ +--[[ +Minimal test framework for Lua. +lester - v0.1.5 - 18/May/2023 +Eduardo Bart - edub4rt@gmail.com +https://github.com/edubart/lester +Minimal Lua test framework. +See end of file for LICENSE. +]] + +--[[-- +Lester is a minimal unit testing framework for Lua with a focus on being simple to use. + +## Features + +* Minimal, just one file. +* Self contained, no external dependencies. +* Simple and hackable when needed. +* Use `describe` and `it` blocks to describe tests. +* Supports `before` and `after` handlers. +* Colored output. +* Configurable via the script or with environment variables. +* Quiet mode, to use in live development. +* Optionally filter tests by name. +* Show traceback on errors. +* Show time to complete tests. +* Works with Lua 5.1+. +* Efficient. + +## Usage + +Copy `lester.lua` file to a project and require it, +which returns a table that includes all of the functionality: + +```lua +local lester = require 'lester' +local describe, it, expect = lester.describe, lester.it, lester.expect + +-- Customize lester configuration. +lester.show_traceback = false + +-- Parse arguments from command line. +lester.parse_args() + +describe('my project', function() + lester.before(function() + -- This function is run before every test. + end) + + describe('module1', function() -- Describe blocks can be nested. + it('feature1', function() + expect.equal('something', 'something') -- Pass. + end) + + it('feature2', function() + expect.truthy(false) -- Fail. + end) + + local feature3_test_enabled = false + it('feature3', function() -- This test will be skipped. + expect.truthy(false) -- Fail. + end, feature3_test_enabled) + end) +end) + +lester.report() -- Print overall statistic of the tests run. +lester.exit() -- Exit with success if all tests passed. +``` + +## Customizing output with environment variables + +To customize the output of lester externally, +you can set the following environment variables before running a test suite: + +* `LESTER_QUIET="true"`, omit print of passed tests. +* `LESTER_COLOR="false"`, disable colored output. +* `LESTER_SHOW_TRACEBACK="false"`, disable traceback on test failures. +* `LESTER_SHOW_ERROR="false"`, omit print of error description of failed tests. +* `LESTER_STOP_ON_FAIL="true"`, stop on first test failure. +* `LESTER_UTF8TERM="false"`, disable printing of UTF-8 characters. +* `LESTER_FILTER="some text"`, filter the tests that should be run. + +Note that these configurations can be changed via script too, check the documentation. + +## Customizing output with command line arguments + +You can also customize output using command line arguments +if `lester.parse_args()` is called at startup. + +The following command line arguments are available: + +* `--quiet`, omit print of passed tests. +* `--no-quiet`, show print of passed tests. +* `--no-color`, disable colored output. +* `--no-show-traceback`, disable traceback on test failures. +* `--no-show-error`, omit print of error description of failed tests. +* `--stop-on-fail`, stop on first test failure. +* `--no-utf8term`, disable printing of UTF-8 characters. +* `--filter="some text"`, filter the tests that should be run. + +]] + +-- Returns whether the terminal supports UTF-8 characters. +local function is_utf8term() + local lang = os.getenv("LANG") + return (lang and lang:lower():match("utf%-?8$")) and true or false +end + +-- Returns whether a system environment variable is "true". +local function getboolenv(varname, default) + local val = os.getenv(varname) + if val == "true" then + return true + elseif val == "false" then + return false + end + return default +end + +-- The lester module. +local lester = { + --- Weather lines of passed tests should not be printed. False by default. + quiet = getboolenv("LESTER_QUIET", false), + --- Weather the output should be colorized. True by default. + color = getboolenv("LESTER_COLOR", true), + --- Weather a traceback must be shown on test failures. True by default. + show_traceback = getboolenv("LESTER_SHOW_TRACEBACK", true), + --- Weather the error description of a test failure should be shown. True by default. + show_error = getboolenv("LESTER_SHOW_ERROR", true), + --- Weather test suite should exit on first test failure. False by default. + stop_on_fail = getboolenv("LESTER_STOP_ON_FAIL", false), + --- Weather we can print UTF-8 characters to the terminal. True by default when supported. + utf8term = getboolenv("LESTER_UTF8TERM", is_utf8term()), + --- A string with a lua pattern to filter tests. Nil by default. + filter = os.getenv("LESTER_FILTER") or "", + --- Function to retrieve time in seconds with milliseconds precision, `os.clock` by default. + seconds = os.clock, +} + +-- Variables used internally for the lester state. +local lester_start = nil +local last_succeeded = false +local level = 0 +local successes = 0 +local total_successes = 0 +local failures = 0 +local total_failures = 0 +local skipped = 0 +local total_skipped = 0 +local start = 0 +local befores = {} +local afters = {} +local names = {} + +-- Color codes. +local color_codes = { + reset = string.char(27) .. "[0m", + bright = string.char(27) .. "[1m", + red = string.char(27) .. "[31m", + green = string.char(27) .. "[32m", + yellow = string.char(27) .. "[33m", + blue = string.char(27) .. "[34m", + magenta = string.char(27) .. "[35m", +} + +local quiet_o_char = string.char(226, 151, 143) + +-- Colors table, returning proper color code if color mode is enabled. +local colors = setmetatable({}, { __index = function(_, key) return lester.color and color_codes[key] or "" end }) + +--- Table of terminal colors codes, can be customized. +lester.colors = colors + +-- Parse command line arguments from `arg` table. +-- It `arg` is nil then the global `arg` is used. +function lester.parse_args(arg) + for _, opt in ipairs(arg or _G.arg) do + local name, value + if opt:find("^%-%-filter") then + name = "filter" + value = opt:match("^%-%-filter%=(.*)$") + elseif opt:find("^%-%-no%-[a-z0-9-]+$") then + name = opt:match("^%-%-no%-([a-z0-9-]+)$"):gsub("-", "_") + value = false + elseif opt:find("^%-%-[a-z0-9-]+$") then + name = opt:match("^%-%-([a-z0-9-]+)$"):gsub("-", "_") + value = true + end + if + value ~= nil + and lester[name] ~= nil + and (type(lester[name]) == "boolean" or type(lester[name]) == "string") + then + lester[name] = value + end + end +end + +--- Describe a block of tests, which consists in a set of tests. +-- Describes can be nested. +-- @param name A string used to describe the block. +-- @param func A function containing all the tests or other describes. +function lester.describe(name, func) + if level == 0 then -- Get start time for top level describe blocks. + failures = 0 + successes = 0 + skipped = 0 + start = lester.seconds() + if not lester_start then lester_start = start end + end + -- Setup describe block variables. + level = level + 1 + names[level] = name + -- Run the describe block. + func() + -- Cleanup describe block. + afters[level] = nil + befores[level] = nil + names[level] = nil + level = level - 1 + -- Pretty print statistics for top level describe block. + if level == 0 and not lester.quiet and (successes > 0 or failures > 0) then + local io_write = io.write + local colors_reset, colors_green = colors.reset, colors.green + io_write( + failures == 0 and colors_green or colors.red, + "[====] ", + colors.magenta, + name, + colors_reset, + " | ", + colors_green, + successes, + colors_reset, + " successes / " + ) + if skipped > 0 then io_write(colors.yellow, skipped, colors_reset, " skipped / ") end + if failures > 0 then io_write(colors.red, failures, colors_reset, " failures / ") end + io_write(colors.bright, string.format("%.6f", lester.seconds() - start), colors_reset, " seconds\n") + end +end + +-- Error handler used to get traceback for errors. +local function xpcall_error_handler(err) return debug.traceback(tostring(err), 2) end + +-- Pretty print the line on the test file where an error happened. +local function show_error_line(err) + local info = debug.getinfo(3) + local io_write = io.write + local colors_reset = colors.reset + local short_src, currentline = info.short_src, info.currentline + io_write(" (", colors.blue, short_src, colors_reset, ":", colors.bright, currentline, colors_reset) + if err and lester.show_traceback then + local fnsrc = short_src .. ":" .. currentline + for cap1, cap2 in err:gmatch("\t[^\n:]+:(%d+): in function <([^>]+)>\n") do + if cap2 == fnsrc then + io_write("/", colors.bright, cap1, colors_reset) + break + end + end + end + io_write(")") +end + +-- Pretty print the test name, with breadcrumb for the describe blocks. +local function show_test_name(name) + local io_write = io.write + local colors_reset = colors.reset + for _, descname in ipairs(names) do + io_write(colors.magenta, descname, colors_reset, " | ") + end + io_write(colors.bright, name, colors_reset) +end + +--- Declare a test, which consists of a set of assertions. +-- @param name A name for the test. +-- @param func The function containing all assertions. +-- @param enabled If not nil and equals to false, the test will be skipped and this will be reported. +function lester.it(name, func, enabled) + -- Skip the test silently if it does not match the filter. + if lester.filter then + local fullname = table.concat(names, " | ") .. " | " .. name + if not fullname:match(lester.filter) then return end + end + local io_write = io.write + local colors_reset = colors.reset + -- Skip the test if it's disabled, while displaying a message + if enabled == false then + if not lester.quiet then + io_write(colors.yellow, "[SKIP] ", colors_reset) + show_test_name(name) + io_write("\n") + else -- Show just a character hinting that the test was skipped. + local o = (lester.utf8term and lester.color) and quiet_o_char or "o" + io_write(colors.yellow, o, colors_reset) + end + skipped = skipped + 1 + total_skipped = total_skipped + 1 + return + end + -- Execute before handlers. + for _, levelbefores in pairs(befores) do + for _, beforefn in ipairs(levelbefores) do + beforefn(name) + end + end + -- Run the test, capturing errors if any. + local success, err + if lester.show_traceback then + success, err = xpcall(func, xpcall_error_handler) + else + success, err = pcall(func) + if not success and err then err = tostring(err) end + end + -- Count successes and failures. + if success then + successes = successes + 1 + total_successes = total_successes + 1 + else + failures = failures + 1 + total_failures = total_failures + 1 + end + -- Print the test run. + if not lester.quiet then -- Show test status and complete test name. + if success then + io_write(colors.green, "[PASS] ", colors_reset) + else + io_write(colors.red, "[FAIL] ", colors_reset) + end + show_test_name(name) + if not success then show_error_line(err) end + io_write("\n") + else + if success then -- Show just a character hinting that the test succeeded. + local o = (lester.utf8term and lester.color) and quiet_o_char or "o" + io_write(colors.green, o, colors_reset) + else -- Show complete test name on failure. + io_write(last_succeeded and "\n" or "", colors.red, "[FAIL] ", colors_reset) + show_test_name(name) + show_error_line(err) + io_write("\n") + end + end + -- Print error message, colorizing its output if possible. + if err and lester.show_error then + if lester.color then + local errfile, errline, errmsg, rest = err:match("^([^:\n]+):(%d+): ([^\n]+)(.*)") + if errfile and errline and errmsg and rest then + io_write(colors.blue, errfile, colors_reset, ":", colors.bright, errline, colors_reset, ": ") + if errmsg:match("^%w([^:]*)$") then + io_write(colors.red, errmsg, colors_reset) + else + io_write(errmsg) + end + err = rest + end + end + io_write(err, "\n\n") + end + io.flush() + -- Stop on failure. + if not success and lester.stop_on_fail then + if lester.quiet then + io_write("\n") + io.flush() + end + lester.exit() + end + -- Execute after handlers. + for _, levelafters in pairs(afters) do + for _, afterfn in ipairs(levelafters) do + afterfn(name) + end + end + last_succeeded = success +end + +--- Set a function that is called before every test inside a describe block. +-- A single string containing the name of the test about to be run will be passed to `func`. +function lester.before(func) + local levelbefores = befores[level] + if not levelbefores then + levelbefores = {} + befores[level] = levelbefores + end + levelbefores[#levelbefores + 1] = func +end + +--- Set a function that is called after every test inside a describe block. +-- A single string containing the name of the test that was finished will be passed to `func`. +-- The function is executed independently if the test passed or failed. +function lester.after(func) + local levelafters = afters[level] + if not levelafters then + levelafters = {} + afters[level] = levelafters + end + levelafters[#levelafters + 1] = func +end + +--- Pretty print statistics of all test runs. +-- With total success, total failures and run time in seconds. +function lester.report() + local now = lester.seconds() + local colors_reset = colors.reset + io.write( + lester.quiet and "\n" or "", + colors.green, + total_successes, + colors_reset, + " successes / ", + colors.yellow, + total_skipped, + colors_reset, + " skipped / ", + colors.red, + total_failures, + colors_reset, + " failures / ", + colors.bright, + string.format("%.6f", now - (lester_start or now)), + colors_reset, + " seconds\n" + ) + io.flush() + return total_failures == 0 +end + +--- Exit the application with success code if all tests passed, or failure code otherwise. +function lester.exit() + -- Collect garbage before exiting to call __gc handlers + collectgarbage() + collectgarbage() + os.exit(total_failures == 0) +end + +local expect = {} +--- Expect module, containing utility function for doing assertions inside a test. +lester.expect = expect + +--- Converts a value to a human-readable string. +-- If the final string not contains only ASCII characters, +-- then it is converted to a Lua hexdecimal string. +function expect.tohumanstring(v) + local s = tostring(v) + if s:find("[^ -~\n\t]") then -- string contains non printable ASCII + return '"' .. s:gsub(".", function(c) return string.format("\\x%02X", c:byte()) end) .. '"' + end + return s +end + +--- Check if a function fails with an error. +-- If `expected` is nil then any error is accepted. +-- If `expected` is a string then we check if the error contains that string. +-- If `expected` is anything else then we check if both are equal. +function expect.fail(func, expected) + local ok, err = pcall(func) + if ok then + error("expected function to fail", 2) + elseif expected ~= nil then + local found = expected == err + if not found and type(expected) == "string" then found = string.find(tostring(err), expected, 1, true) end + if not found then + error("expected function to fail\nexpected:\n" .. tostring(expected) .. "\ngot:\n" .. tostring(err), 2) + end + end +end + +--- Check if a function does not fail with a error. +function expect.not_fail(func) + local ok, err = pcall(func) + if not ok then error("expected function to not fail\ngot error:\n" .. expect.tohumanstring(err), 2) end +end + +--- Check if a value is not `nil`. +function expect.exist(v) + if v == nil then error("expected value to exist\ngot:\n" .. expect.tohumanstring(v), 2) end +end + +--- Check if a value is `nil`. +function expect.not_exist(v) + if v ~= nil then error("expected value to not exist\ngot:\n" .. expect.tohumanstring(v), 2) end +end + +--- Check if an expression is evaluates to `true`. +function expect.truthy(v) + if not v then error("expected expression to be true\ngot:\n" .. expect.tohumanstring(v), 2) end +end + +--- Check if an expression is evaluates to `false`. +function expect.falsy(v) + if v then error("expected expression to be false\ngot:\n" .. expect.tohumanstring(v), 2) end +end + +--- Returns raw tostring result for a value. +local function rawtostring(v) + local mt = getmetatable(v) + if mt then setmetatable(v, nil) end + local s = tostring(v) + if mt then setmetatable(v, mt) end + return s +end + +-- Returns key suffix for a string_eq table key. +local function strict_eq_key_suffix(k) + if type(k) == "string" then + if k:find("^[a-zA-Z_][a-zA-Z0-9]*$") then -- string is a lua field + return "." .. k + elseif k:find("[^ -~\n\t]") then -- string contains non printable ASCII + return '["' .. k:gsub(".", function(c) return string.format("\\x%02X", c:byte()) end) .. '"]' + else + return '["' .. k .. '"]' + end + else + return string.format("[%s]", rawtostring(k)) + end +end + +--- Compare if two values are equal, considering nested tables. +function expect.strict_eq(t1, t2, name) + if rawequal(t1, t2) then return true end + name = name or "value" + local t1type, t2type = type(t1), type(t2) + if t1type ~= t2type then + return false, string.format("expected types to be equal for %s\nfirst: %s\nsecond: %s", name, t1type, t2type) + end + if t1type == "table" then + if getmetatable(t1) ~= getmetatable(t2) then + return false, + string.format( + "expected metatables to be equal for %s\nfirst: %s\nsecond: %s", + name, + expect.tohumanstring(t1), + expect.tohumanstring(t2) + ) + end + for k, v1 in pairs(t1) do + local ok, err = expect.strict_eq(v1, t2[k], name .. strict_eq_key_suffix(k)) + if not ok then return false, err end + end + for k, v2 in pairs(t2) do + local ok, err = expect.strict_eq(v2, t1[k], name .. strict_eq_key_suffix(k)) + if not ok then return false, err end + end + elseif t1 ~= t2 then + return false, + string.format( + "expected values to be equal for %s\nfirst:\n%s\nsecond:\n%s", + name, + expect.tohumanstring(t1), + expect.tohumanstring(t2) + ) + end + return true +end + +--- Check if two values are equal. +function expect.equal(v1, v2) + local ok, err = expect.strict_eq(v1, v2) + if not ok then error(err, 2) end +end + +--- Check if two values are not equal. +function expect.not_equal(v1, v2) + if expect.strict_eq(v1, v2) then + local v1s, v2s = expect.tohumanstring(v1), expect.tohumanstring(v2) + error("expected values to be not equal\nfirst value:\n" .. v1s .. "\nsecond value:\n" .. v2s, 2) + end +end + +return lester + +--[[ +The MIT License (MIT) + +Copyright (c) 2021-2023 Eduardo Bart (https://github.com/edubart) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +]]