瀏覽代碼

lib: move rustsbi/sbi-testing repo to rustsbi/rustsbi

Signed-off-by: DongQing <placebo27@hust.edu.cn>
DongQing 1 年之前
父節點
當前提交
e657d5e86a

+ 38 - 0
.github/workflows/rust.yml

@@ -163,3 +163,41 @@ jobs:
         run: cargo build -p sbi-spec
       - name: Check test
         run: cargo test -p sbi-spec
+
+# sbi-testing:
+#     name: Run rust-clippy analyzing
+#     runs-on: ubuntu-latest
+#     permissions:
+#       security-events: write
+#     steps:
+#       - name: Checkout code
+#         uses: actions/checkout@v3
+
+#       - name: Check format
+#         run: cargo fmt --check
+
+#       - name: Install clippy-sarif
+#         uses: actions-rs/install@v0.1
+#         with:
+#           crate: clippy-sarif
+#           version: latest
+
+#       - name: Install sarif-fmt
+#         uses: actions-rs/install@v0.1
+#         with:
+#           crate: sarif-fmt
+#           version: latest
+
+#       - name: Run rust-clippy
+#         run:
+#           cargo clippy
+#           --all-featuers
+#           --package fast-trap
+#           --message-format=json | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt
+#         continue-on-error: true
+
+#       - name: Upload analysis results to GitHub
+#         uses: github/codeql-action/upload-sarif@v2
+#         with:
+#           sarif_file: rust-clippy-results.sarif
+#           wait-for-processing: true

+ 1 - 1
CHANGELOG.md

@@ -22,7 +22,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 - run on provided `EnvInfo` by default; bare metal M-mode environment should gate `machine`
 - doc: grammar tweaks in hsm module
 - update dependency `sbi-spec` to v0.0.6, use `Physical` struct from `sbi-spec` crate.
-- merge rustsbi/sbi-rt and rustsbi/sbi-spec repositories into rustsbi/rustsbi repository.
+- merge rustsbi/sbi-{rt, spec, testing} repositories into rustsbi/rustsbi repository.
 
 ### Removed
 

+ 1 - 0
Cargo.toml

@@ -4,5 +4,6 @@ members = [
     "rustsbi/macros",
     "sbi-rt",
     "sbi-spec",
+    "sbi-testing",
 ]
 resolver = "2"

+ 2 - 0
sbi-testing/.cargo/config.toml

@@ -0,0 +1,2 @@
+[build]
+target = "riscv64imac-unknown-none-elf"

+ 7 - 0
sbi-testing/.gitignore

@@ -0,0 +1,7 @@
+**/.*/*
+!/.github/*
+!/.cargo/*
+!/.vscode/settings.json
+
+/target
+/Cargo.lock

+ 37 - 0
sbi-testing/CHANGELOG.md

@@ -0,0 +1,37 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres
+to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+
+### Added
+
+- Test new extension DBCN
+
+### Modified
+
+- Update sbi-spec to version 0.0.6
+- Update sbi-rt to version 0.0.3
+
+### Fixed
+
+## [0.0.2] - 2023-01-20
+
+### Modified
+
+- Update dependency crate `riscv` to version 0.10.1
+- Remove feature declaration `asm_sym`, bump MSRV to 1.66.0
+
+## [0.0.1] - 2022-10-10
+
+### Modified
+
+- Project structure to keep test functions at root module
+- Use `sbi-rt` v0.0.2 and `sbi-spec` v0.0.4
+
+[Unreleased]: https://github.com/rustsbi/sbi-testing/compare/v0.0.2...HEAD
+[0.0.2]: https://github.com/rustsbi/sbi-testing/compare/v0.0.1...v0.0.2
+[0.0.1]: https://github.com/rustsbi/sbi-testing/compare/v0.0.0...v0.0.1

+ 27 - 0
sbi-testing/Cargo.toml

@@ -0,0 +1,27 @@
+[package]
+name = "sbi-testing"
+version = "0.0.3-rc.0"
+edition = "2021"
+description = "Provide a set of test cases for supervisors to verify functions of the supervisor executation environment"
+categories = ["os", "no-std"]
+keywords = ["riscv", "sbi", "rustsbi"]
+authors = ["YdrMaster <ydrml@hotmail.com>"]
+repository = "https://github.com/rustsbi/sbi-testing"
+documentation = "https://docs.rs/sbi-testing"
+license = "MulanPSL-2.0 OR MIT"
+readme = "README.md"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[package.metadata.docs.rs]
+default-target = "riscv64imac-unknown-none-elf"
+targets = ["riscv32imac-unknown-none-elf", "riscv64imac-unknown-none-elf"]
+
+[dependencies]
+sbi-rt = "0.0.3-rc.5"
+sbi-spec = "0.0.7-alpha.3"
+riscv = "0.10.1"
+log_crate = { version = "0.4", package = "log", optional = true }
+
+[features]
+log = ["log_crate"]

+ 9 - 0
sbi-testing/LICENSE-MIT

@@ -0,0 +1,9 @@
+The MIT License (MIT)
+
+Copyright © 2022 YdrMaster
+
+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.

+ 101 - 0
sbi-testing/LICENSE-MULAN

@@ -0,0 +1,101 @@
+木兰宽松许可证, 第2版
+
+2020年1月 <http://license.coscl.org.cn/MulanPSL2>
+
+您对“软件”的复制、使用、修改及分发受木兰宽松许可证,第2版(“本许可证”)的如下条款的约束:
+
+0. 定义
+
+“软件”是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。
+
+“贡献”是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。
+
+“贡献者”是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。
+
+“法人实体”是指提交贡献的机构及其“关联实体”。
+
+“关联实体”是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的控制是指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。
+
+1. 授予版权许可
+
+每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可以复制、使用、修改、分发其“贡献”,不论修改与否。
+
+2. 授予专利许可
+
+每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权行动之日终止。
+
+3. 无商标许可
+
+“本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可,但您为满足第4条规定的声明义务而必须使用除外。
+
+4. 分发限制
+
+您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。
+
+5. 免责声明与责任限制
+
+“软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。
+
+6. 语言
+
+“本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文版为准。
+
+条款结束
+
+如何将木兰宽松许可证,第2版,应用到您的软件
+
+如果您希望将木兰宽松许可证,第2版,应用到您的新软件,为了方便接收者查阅,建议您完成如下三步:
+
+1,请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字;
+
+2,请您在软件包的一级目录下创建以“LICENSE”为名的文件,将整个许可证文本放入该文件中;
+
+3,请将如下声明文本放入每个源文件的头部注释中。
+
+Copyright (c) 2020 Luo Jia
+
+RustSBI is licensed under Mulan PSL v2. You can use this software according to the terms and conditions of the Mulan PSL v2. You may obtain a copy of Mulan PSL v2 at: <http://license.coscl.org.cn/MulanPSL2>
+
+THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. See the Mulan PSL v2 for more details.
+
+January 2020 <http://license.coscl.org.cn/MulanPSL2>
+
+Your reproduction, use, modification and distribution of the Software shall be subject to Mulan PSL v2 (this License) with the following terms and conditions:
+
+0. Definition
+
+Software means the program and related documents which are licensed under this License and comprise all Contribution(s).
+
+Contribution means the copyrightable work licensed by a particular Contributor under this License.
+
+Contributor means the Individual or Legal Entity who licenses its copyrightable work under this License.
+
+Legal Entity means the entity making a Contribution and all its Affiliates.
+
+Affiliates means entities that control, are controlled by, or are under common control with the acting entity under this License, ‘control’ means direct or indirect ownership of at least fifty percent (50%) of the voting power, capital or other securities of controlled or commonly controlled entity.
+
+1. Grant of Copyright License
+
+Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable copyright license to reproduce, use, modify, or distribute its Contribution, with modification or not.
+
+2. Grant of Patent License
+
+Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable (except for revocation under this Section) patent license to make, have made, use, offer for sale, sell, import or otherwise transfer its Contribution, where such patent license is only limited to the patent claims owned or controlled by such Contributor now or in future which will be necessarily infringed by its Contribution alone, or by combination of the Contribution with the Software to which the Contribution was contributed. The patent license shall not apply to any modification of the Contribution, and any other combination which includes the Contribution. If you or your Affiliates directly or indirectly institute patent litigation (including a cross claim or counterclaim in a litigation) or other patent enforcement activities against any individual or entity by alleging that the Software or any Contribution in it infringes patents, then any patent license granted to you under this License for the Software shall terminate as of the date such litigation or activity is filed or taken.
+
+3. No Trademark License
+
+No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor, except as required to fulfill notice requirements in Section 4.
+
+4. Distribution Restriction
+
+You may distribute the Software in any medium with or without modification, whether in source or executable forms, provided that you provide recipients with a copy of this License and retain copyright, patent, trademark and disclaimer statements in the Software.
+
+5. Disclaimer of Warranty and Limitation of Liability
+
+THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO MATTER HOW IT’S CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+6. Language
+
+THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION SHALL PREVAIL.
+
+END OF THE TERMS AND CONDITIONS

+ 21 - 0
sbi-testing/README.md

@@ -0,0 +1,21 @@
+# SBI 功能测试集
+
+[![CI](https://github.com/rustsbi/sbi-testing/actions/workflows/workflow.yml/badge.svg?branch=main)](https://github.com/rustsbi/sbi-testing/actions)
+[![Latest version](https://img.shields.io/crates/v/sbi-testing.svg)](https://crates.io/crates/sbi-testing)
+[![issue](https://img.shields.io/github/issues/rustsbi/sbi-testing)](https://github.com/rustsbi/sbi-testing/issues)
+[![Documentation](https://docs.rs/sbi-testing/badge.svg)](https://docs.rs/sbi-testing)
+![license](https://img.shields.io/github/license/rustsbi/sbi-testing)
+
+- [An English README](README_EN.md)
+
+这个库封装了一系列测试,供特权软件测试为自己服务的特权运行环境功能是否正常。
+
+SBI 1.0.0 标准各章节的实现情况:
+
+- [x] §4  Base
+- [x] §6  TIME
+- [x] §7  sPI
+- [ ] §8  RFNC
+- [x] §9  HSM
+- [ ] §10 SRST
+- [ ] §11 PMU

+ 21 - 0
sbi-testing/README_EN.md

@@ -0,0 +1,21 @@
+# SBI Testing
+
+[![CI](https://github.com/rustsbi/sbi-testing/actions/workflows/workflow.yml/badge.svg?branch=main)](https://github.com/rustsbi/sbi-testing/actions)
+[![Latest version](https://img.shields.io/crates/v/sbi-testing.svg)](https://crates.io/crates/sbi-testing)
+[![issue](https://img.shields.io/github/issues/rustsbi/sbi-testing)](https://github.com/rustsbi/sbi-testing/issues)
+[![Documentation](https://docs.rs/sbi-testing/badge.svg)](https://docs.rs/sbi-testing)
+![license](https://img.shields.io/github/license/rustsbi/sbi-testing)
+
+- [中文自述文件](README.md)
+
+This library provides a set of test cases for supervisors to verify functions of the supervisor executation environment.
+
+Characters implementation of SBI 1.0.0 specification:
+
+- [x] §4  Base
+- [x] §6  TIME
+- [x] §7  sPI
+- [ ] §8  RFNC
+- [x] §9  HSM
+- [ ] §10 SRST
+- [ ] §11 PMU

+ 5 - 0
sbi-testing/rust-toolchain.toml

@@ -0,0 +1,5 @@
+[toolchain]
+profile = "minimal"
+channel = "nightly"
+components = ["rust-src", "llvm-tools-preview", "rustfmt", "clippy"]
+targets = ["riscv64imac-unknown-none-elf"]

+ 106 - 0
sbi-testing/src/base.rs

@@ -0,0 +1,106 @@
+//! RISC-V SBI Base extension test suite.
+
+use sbi::{ExtensionInfo, Version};
+use sbi_spec::base::impl_id;
+
+/// Base extension test cases.
+#[derive(Clone, Debug)]
+pub enum Case {
+    /// Can't procceed test for base extension does not exist.
+    NotExist,
+    /// Test begin
+    Begin,
+    /// Test process for getting SBI specification version.
+    GetSbiSpecVersion(Version),
+    /// Test process for getting SBI implementation ID.
+    GetSbiImplId(Result<&'static str, usize>),
+    /// Test process for getting version of SBI implementation.
+    GetSbiImplVersion(usize),
+    /// Test process for probe standard SBI extensions.
+    ProbeExtensions(Extensions),
+    /// Test process for getting vendor ID from RISC-V environment.
+    GetMVendorId(usize),
+    /// Test process for getting architecture ID from RISC-V environment.
+    GetMArchId(usize),
+    /// Test process for getting implementation ID from RISC-V environment.
+    GetMimpId(usize),
+    /// All test cases on base module finished.
+    Pass,
+}
+
+/// Information about all SBI standard extensions.
+#[derive(Clone, Debug)]
+pub struct Extensions {
+    /// Timer programmer extension.
+    pub time: ExtensionInfo,
+    /// Inter-processor Interrupt extension.
+    pub spi: ExtensionInfo,
+    /// Remote Fence extension.
+    pub rfnc: ExtensionInfo,
+    /// Hart State Monitor extension.
+    pub hsm: ExtensionInfo,
+    /// System Reset extension.
+    pub srst: ExtensionInfo,
+    /// Performance Monitor Unit extension.
+    pub pmu: ExtensionInfo,
+}
+
+impl core::fmt::Display for Extensions {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        write!(f, "[Base")?;
+        if self.time.is_available() {
+            write!(f, ", TIME")?;
+        }
+        if self.spi.is_available() {
+            write!(f, ", sPI")?;
+        }
+        if self.rfnc.is_available() {
+            write!(f, ", RFNC")?;
+        }
+        if self.hsm.is_available() {
+            write!(f, ", HSM")?;
+        }
+        if self.srst.is_available() {
+            write!(f, ", SRST")?;
+        }
+        if self.pmu.is_available() {
+            write!(f, ", PMU")?;
+        }
+        write!(f, "]")
+    }
+}
+
+/// Test base extension.
+///
+/// The test case output would be handled in `f`.
+pub fn test(mut f: impl FnMut(Case)) {
+    if sbi::probe_extension(sbi::Base).is_unavailable() {
+        f(Case::NotExist);
+        return;
+    }
+    f(Case::Begin);
+    f(Case::GetSbiSpecVersion(sbi::get_spec_version()));
+    f(Case::GetSbiImplId(match sbi::get_sbi_impl_id() {
+        impl_id::BBL => Ok("BBL"),
+        impl_id::OPEN_SBI => Ok("OpenSBI"),
+        impl_id::XVISOR => Ok("Xvisor"),
+        impl_id::KVM => Ok("KVM"),
+        impl_id::RUST_SBI => Ok("RustSBI"),
+        impl_id::DIOSIX => Ok("Diosix"),
+        impl_id::COFFER => Ok("Coffer"),
+        unknown => Err(unknown),
+    }));
+    f(Case::GetSbiImplVersion(sbi::get_sbi_impl_version()));
+    f(Case::ProbeExtensions(Extensions {
+        time: sbi::probe_extension(sbi::Timer),
+        spi: sbi::probe_extension(sbi::Ipi),
+        rfnc: sbi::probe_extension(sbi::Fence),
+        hsm: sbi::probe_extension(sbi::Hsm),
+        srst: sbi::probe_extension(sbi::Reset),
+        pmu: sbi::probe_extension(sbi::Pmu),
+    }));
+    f(Case::GetMVendorId(sbi::get_mvendorid()));
+    f(Case::GetMArchId(sbi::get_marchid()));
+    f(Case::GetMimpId(sbi::get_mimpid()));
+    f(Case::Pass);
+}

+ 66 - 0
sbi-testing/src/dbcn.rs

@@ -0,0 +1,66 @@
+//! Debug console extension test suite.
+
+use sbi::SbiRet;
+use sbi_spec::binary::Physical;
+
+/// Debug console extension test cases.
+#[derive(Clone, Debug)]
+pub enum Case {
+    /// Can't procceed test for debug console extension does not exist.
+    NotExist,
+    /// Test begin.
+    Begin,
+    /// Test process for write a byte to console.
+    WriteByte,
+    /// Test failed for can't write byte.
+    WritingByteFailed(SbiRet),
+    /// Test process for write complete slice.
+    WriteSlice,
+    /// Test process for write partial slice.
+    WritingPartialSlice(usize),
+    /// Test failed for can't write slice.
+    WritingSliceFailed(SbiRet),
+    /// Test process for read some bytes from console.
+    Read(usize),
+    /// Test failed for can't read to buffer.
+    ReadingFailed(SbiRet),
+    /// All test cases on debug console extension has passed.
+    Pass,
+}
+
+/// Test debug console extension.
+pub fn test(mut f: impl FnMut(Case)) {
+    if sbi::probe_extension(sbi::Console).is_unavailable() {
+        f(Case::NotExist);
+        return;
+    }
+
+    f(Case::Begin);
+
+    let ret = sbi::console_write_byte(b'H');
+    if ret.is_ok() {
+        f(Case::WriteByte);
+    } else {
+        f(Case::WritingByteFailed(ret));
+    }
+    let words = b"ello, world!\r\n";
+    let ret = sbi::console_write(Physical::new(words.len(), words.as_ptr() as _, 0));
+    if let Some(len) = ret.ok() {
+        f(if len == words.len() {
+            Case::WriteSlice
+        } else {
+            Case::WritingPartialSlice(len)
+        });
+    } else {
+        f(Case::WritingSliceFailed(ret));
+    }
+    let mut buffer = [0u8; 16];
+    let ret = sbi::console_read(Physical::new(buffer.len(), buffer.as_mut_ptr() as _, 0));
+    if let Some(len) = ret.ok() {
+        f(Case::Read(len));
+    } else {
+        f(Case::ReadingFailed(ret));
+    }
+
+    f(Case::Pass);
+}

+ 276 - 0
sbi-testing/src/hsm.rs

@@ -0,0 +1,276 @@
+//! Hart state monitor extension test suite.
+
+use core::sync::atomic::{AtomicU32, Ordering};
+use sbi::SbiRet;
+use sbi_spec::hsm::hart_state;
+
+/// Hart state monitor extension test cases.
+#[derive(Clone, Debug)]
+pub enum Case<'a> {
+    /// Can't procceed test for Hart state monitor extension does not exist.
+    NotExist,
+    /// Test begin
+    Begin,
+    /// Test failed for hart started before test begin.
+    ///
+    /// The returned value includes which hart led to this test failure.
+    HartStartedBeforeTest(usize),
+    /// Test failed for no other harts are available to be tested.
+    NoStoppedHart,
+    /// Test process for begin test hart state monitor on one batch.
+    BatchBegin(&'a [usize]),
+    /// Test process for target hart to be tested has started.
+    HartStarted(usize),
+    /// Test failed for can't start target hart with [`SbiRet`] error.
+    HartStartFailed {
+        /// The target hart ID that has failed to start.
+        hartid: usize,
+        /// The `SbiRet` value for the failed hart start SBI call.
+        ret: SbiRet,
+    },
+    /// Test process for target hart to be tested has non-retentively suspended.
+    HartSuspendedNonretentive(usize),
+    /// Test process for target hart to be tested has resumed.
+    HartResumed(usize),
+    /// Test process for target hart to be tested has retentively suspended.
+    HartSuspendedRetentive(usize),
+    /// Test process for target hart to be tested has stopped.
+    HartStopped(usize),
+    /// Test process for harts on current batch has passed the tests.
+    BatchPass(&'a [usize]),
+    /// All test cases on hart state monitor module finished.
+    Pass,
+}
+
+/// Test hart state monitor extension on given harts.
+///
+/// The test case output is to be handled in `f`.
+pub fn test(
+    primary_hart_id: usize,
+    mut hart_mask: usize,
+    hart_mask_base: usize,
+    mut f: impl FnMut(Case),
+) {
+    // 不支持 HSM 扩展
+    if sbi::probe_extension(sbi::Hsm).is_unavailable() {
+        f(Case::NotExist);
+        return;
+    }
+    f(Case::Begin);
+    // 分批测试
+
+    let mut batch = [0usize; TEST_BATCH_SIZE];
+    let mut batch_count = 0;
+    let mut batch_size = 0;
+    let mut hartid = hart_mask_base;
+    while hart_mask != 0 {
+        if hartid != primary_hart_id {
+            // 副核在测试前必须处于停止状态
+            if sbi::hart_get_status(hartid) == STOPPED {
+                batch[batch_size] = hartid;
+                batch_size += 1;
+                // 收集一个批次,执行测试
+                if batch_size == TEST_BATCH_SIZE {
+                    if test_batch(&batch, &mut f) {
+                        batch_count += 1;
+                        batch_size = 0;
+                    } else {
+                        return;
+                    }
+                }
+            }
+            // 副核不在停止状态
+            else {
+                f(Case::HartStartedBeforeTest(hartid));
+            }
+        }
+        let distance = hart_mask.trailing_zeros() + 1;
+        hart_mask >>= distance;
+        hartid += distance as usize;
+    }
+    // 为不满一批次的核执行测试
+    if batch_size > 0 {
+        if test_batch(&batch[..batch_size], &mut f) {
+            f(Case::Pass);
+        }
+    }
+    // 所有批次通过测试
+    else if batch_count > 0 {
+        f(Case::Pass);
+    }
+    // 没有找到能参与测试的副核
+    else {
+        f(Case::NoStoppedHart)
+    }
+}
+
+const STARTED: SbiRet = SbiRet::success(hart_state::STARTED);
+const STOPPED: SbiRet = SbiRet::success(hart_state::STOPPED);
+const SUSPENDED: SbiRet = SbiRet::success(hart_state::SUSPENDED);
+
+const TEST_BATCH_SIZE: usize = 4;
+static mut STACK: [ItemPerHart; TEST_BATCH_SIZE] = [ItemPerHart::ZERO; TEST_BATCH_SIZE];
+
+#[repr(C, align(512))]
+struct ItemPerHart {
+    stage: AtomicU32,
+    signal: AtomicU32,
+    stack: [u8; 504],
+}
+
+const STAGE_IDLE: u32 = 0;
+const STAGE_STARTED: u32 = 1;
+const STAGE_RESUMED: u32 = 2;
+
+impl ItemPerHart {
+    #[allow(clippy::declare_interior_mutable_const)]
+    const ZERO: Self = Self {
+        stage: AtomicU32::new(STAGE_IDLE),
+        signal: AtomicU32::new(0),
+        stack: [0; 504],
+    };
+
+    #[inline]
+    fn reset(&mut self) -> *const ItemPerHart {
+        self.stage.store(STAGE_IDLE, Ordering::Relaxed);
+        self as _
+    }
+
+    #[inline]
+    fn wait_start(&self) {
+        while self.stage.load(Ordering::Relaxed) != STAGE_STARTED {
+            core::hint::spin_loop();
+        }
+    }
+
+    #[inline]
+    fn wait_resume(&self) {
+        while self.stage.load(Ordering::Relaxed) != STAGE_RESUMED {
+            core::hint::spin_loop();
+        }
+    }
+
+    #[inline]
+    fn send_signal(&self) {
+        self.signal.store(1, Ordering::Release);
+    }
+
+    #[inline]
+    fn wait_signal(&self) {
+        while self
+            .signal
+            .compare_exchange(1, 0, Ordering::Relaxed, Ordering::Relaxed)
+            .is_err()
+        {
+            core::hint::spin_loop();
+        }
+    }
+}
+
+/// 测试一批核
+fn test_batch(batch: &[usize], mut f: impl FnMut(Case)) -> bool {
+    f(Case::BatchBegin(batch));
+    // 初始这些核都是停止状态,测试 start
+    for (i, hartid) in batch.iter().copied().enumerate() {
+        let ptr = unsafe { STACK[i].reset() };
+        let ret = sbi::hart_start(hartid, test_entry as _, ptr as _);
+        if ret.is_err() {
+            f(Case::HartStartFailed { hartid, ret });
+            return false;
+        }
+    }
+    // 测试不可恢复休眠
+    for (i, hartid) in batch.iter().copied().enumerate() {
+        let item = unsafe { &mut STACK[i] };
+        // 等待完成启动
+        while sbi::hart_get_status(hartid) != STARTED {
+            core::hint::spin_loop();
+        }
+        f(Case::HartStarted(hartid));
+        // 等待信号
+        item.wait_start();
+        // 发出信号
+        item.send_signal();
+        // 等待完成休眠
+        while sbi::hart_get_status(hartid) != SUSPENDED {
+            core::hint::spin_loop();
+        }
+        f(Case::HartSuspendedNonretentive(hartid));
+    }
+    // 全部唤醒
+    let mut mask = 1usize;
+    for hartid in &batch[1..] {
+        mask |= 1 << (hartid - batch[0]);
+    }
+    sbi::send_ipi(sbi_spec::binary::HartMask::from_mask_base(mask, batch[0]));
+    // 测试可恢复休眠
+    for (i, hartid) in batch.iter().copied().enumerate() {
+        let item = unsafe { &mut STACK[i] };
+        // 等待完成恢复
+        while sbi::hart_get_status(hartid) != STARTED {
+            core::hint::spin_loop();
+        }
+        f(Case::HartResumed(hartid));
+        // 等待信号
+        item.wait_resume();
+        // 发出信号
+        item.send_signal();
+        // 等待完成休眠
+        while sbi::hart_get_status(hartid) != SUSPENDED {
+            core::hint::spin_loop();
+        }
+        f(Case::HartSuspendedRetentive(hartid));
+        // 单独恢复
+        sbi::send_ipi(sbi_spec::binary::HartMask::from_mask_base(1, hartid));
+        // 等待关闭
+        while sbi::hart_get_status(hartid) != STOPPED {
+            core::hint::spin_loop();
+        }
+        f(Case::HartStopped(hartid));
+    }
+    f(Case::BatchPass(batch));
+    true
+}
+
+/// 测试用启动入口
+#[naked]
+unsafe extern "C" fn test_entry(hartid: usize, opaque: *mut ItemPerHart) -> ! {
+    core::arch::asm!(
+        "csrw sie, zero",   // 关中断
+        "call {set_stack}", // 设置栈
+        "j    {rust_main}", // 进入 rust
+        set_stack = sym set_stack,
+        rust_main = sym rust_main,
+        options(noreturn),
+    )
+}
+
+#[naked]
+unsafe extern "C" fn set_stack(hart_id: usize, ptr: *const ItemPerHart) {
+    core::arch::asm!("addi sp, a1, 512", "ret", options(noreturn));
+}
+
+#[inline(never)]
+extern "C" fn rust_main(hart_id: usize, opaque: *mut ItemPerHart) -> ! {
+    let item = unsafe { &mut *opaque };
+    match item.stage.compare_exchange(
+        STAGE_IDLE,
+        STAGE_STARTED,
+        Ordering::AcqRel,
+        Ordering::Acquire,
+    ) {
+        Ok(_) => {
+            item.wait_signal();
+            let ret = sbi::hart_suspend(sbi::NonRetentive, test_entry as _, opaque as _);
+            unreachable!("suspend [{hart_id}] but {ret:?}")
+        }
+        Err(STAGE_STARTED) => {
+            item.stage.store(STAGE_RESUMED, Ordering::Release);
+            item.wait_signal();
+            let _ = sbi::hart_suspend(sbi::Retentive, test_entry as _, opaque as _);
+            let ret = sbi::hart_stop();
+            unreachable!("suspend [{hart_id}] but {ret:?}")
+        }
+        Err(_) => unreachable!(),
+    }
+}

+ 40 - 0
sbi-testing/src/lib.rs

@@ -0,0 +1,40 @@
+//! RISC-V Supervisor Binary Interface test suite
+
+#![no_std]
+#![deny(warnings, missing_docs)]
+#![feature(naked_functions, asm_const)]
+
+mod thread;
+
+pub extern crate sbi_rt as sbi;
+
+#[cfg(feature = "log")]
+pub extern crate log_crate as log;
+
+#[cfg(feature = "log")]
+mod log_test;
+
+#[cfg(feature = "log")]
+pub use log_test::Testing;
+
+// §4
+mod base;
+pub use base::{test as test_base, Case as BaseCase, Extensions};
+// §6
+mod time;
+pub use time::{test as test_timer, Case as TimerCase};
+// §7
+mod spi;
+pub use spi::{test as test_ipi, Case as IpiCase};
+// §8
+// pub mod rfnc;
+// §9
+mod hsm;
+pub use hsm::{test as test_hsm, Case as HsmCase};
+// §10
+// pub mod srst;
+// §11
+// pub mod pmu;
+// §12
+mod dbcn;
+pub use dbcn::{test as test_dbcn, Case as DbcnCase};

+ 167 - 0
sbi-testing/src/log_test.rs

@@ -0,0 +1,167 @@
+use crate::{base, dbcn, hsm, spi, time};
+use log_crate::*;
+
+/// Automatic SBI testing with logging enabled.
+pub struct Testing {
+    /// The hart ID to test most of single core extensions.
+    pub hartid: usize,
+    /// A list of harts to test Hart State Monitor extension.
+    pub hart_mask: usize,
+    /// Base of hart list to test Hart State Monitor extension.
+    pub hart_mask_base: usize,
+    /// Delay value to test Timer programmer extension.
+    pub delay: u64,
+}
+
+const TARGET: &str = "sbi-testing";
+
+impl Testing {
+    /// Start testing process of RISC-V SBI implementation.
+    pub fn test(self) -> bool {
+        let mut result = true;
+        base::test(|case| {
+            use base::Case::*;
+            match case {
+                NotExist => panic!("Sbi `Base` not exist"),
+                Begin => info!(target: TARGET, "Testing `Base`"),
+                Pass => info!(target: TARGET, "Sbi `Base` test pass"),
+                GetSbiSpecVersion(version) => {
+                    info!(target: TARGET, "sbi spec version = {version}");
+                }
+                GetSbiImplId(Ok(name)) => {
+                    info!(target: TARGET, "sbi impl = {name}");
+                }
+                GetSbiImplId(Err(unknown)) => {
+                    warn!(target: TARGET, "unknown sbi impl = {unknown:#x}");
+                }
+                GetSbiImplVersion(version) => {
+                    info!(target: TARGET, "sbi impl version = {version:#x}");
+                }
+                ProbeExtensions(exts) => {
+                    info!(target: TARGET, "sbi extensions = {exts}");
+                }
+                GetMVendorId(id) => {
+                    info!(target: TARGET, "mvendor id = {id:#x}");
+                }
+                GetMArchId(id) => {
+                    info!(target: TARGET, "march id = {id:#x}");
+                }
+                GetMimpId(id) => {
+                    info!(target: TARGET, "mimp id = {id:#x}");
+                }
+            }
+        });
+        time::test(self.delay, |case| {
+            use time::Case::*;
+            match case {
+                NotExist => {
+                    error!(target: TARGET, "Sbi `TIME` not exist");
+                    result = false;
+                }
+                Begin => info!(target: TARGET, "Testing `TIME`"),
+                Pass => info!(target: TARGET, "Sbi `TIME` test pass"),
+                Interval { begin: _, end: _ } => {
+                    info!(
+                        target: TARGET,
+                        "read time register successfuly, set timer +1s"
+                    );
+                }
+                ReadFailed => {
+                    error!(target: TARGET, "csrr time failed");
+                    result = false;
+                }
+                TimeDecreased { a, b } => {
+                    error!(target: TARGET, "time decreased: {a} -> {b}");
+                    result = false;
+                }
+                SetTimer => {
+                    info!(target: TARGET, "timer interrupt delegate successfuly");
+                }
+                UnexpectedTrap(trap) => {
+                    error!(
+                        target: TARGET,
+                        "expect trap at supervisor timer, but {trap:?} was caught"
+                    );
+                    result = false;
+                }
+            }
+        });
+        spi::test(self.hartid, |case| {
+            use spi::Case::*;
+            match case {
+                NotExist => {
+                    error!(target: TARGET, "Sbi `sPI` not exist");
+                    result = false;
+                }
+                Begin => info!(target: TARGET, "Testing `sPI`"),
+                Pass => info!(target: TARGET, "Sbi `sPI` test pass"),
+                SendIpi => info!(target: TARGET, "send ipi successfuly"),
+                UnexpectedTrap(trap) => {
+                    error!(
+                        target: TARGET,
+                        "expect trap at supervisor soft, but {trap:?} was caught"
+                    );
+                    result = false;
+                }
+            }
+        });
+        hsm::test(self.hartid, self.hart_mask, self.hart_mask_base, |case| {
+            use hsm::Case::*;
+            match case {
+                NotExist => {
+                    error!(target: TARGET, "Sbi `HSM` not exist");
+                    result = false;
+                }
+                Begin => info!(target: TARGET, "Testing `HSM`"),
+                Pass => info!(target: TARGET, "Sbi `HSM` test pass"),
+                HartStartedBeforeTest(id) => warn!(target: TARGET, "hart {id} already started"),
+                NoStoppedHart => warn!(target: TARGET, "no stopped hart"),
+                BatchBegin(batch) => info!(target: TARGET, "Testing harts: {batch:?}"),
+                HartStarted(id) => debug!(target: TARGET, "hart {id} started"),
+                HartStartFailed { hartid, ret } => {
+                    error!(target: TARGET, "hart {hartid} start failed: {ret:?}");
+                    result = false;
+                }
+                HartSuspendedNonretentive(id) => {
+                    debug!(target: TARGET, "hart {id} suspended nonretentive")
+                }
+                HartResumed(id) => debug!(target: TARGET, "hart {id} resumed"),
+                HartSuspendedRetentive(id) => {
+                    debug!(target: TARGET, "hart {id} suspended retentive")
+                }
+                HartStopped(id) => debug!(target: TARGET, "hart {id} stopped"),
+                BatchPass(batch) => info!(target: TARGET, "Testing Pass: {batch:?}"),
+            }
+        });
+        dbcn::test(|case| {
+            use dbcn::Case::*;
+            match case {
+                NotExist => {
+                    error!(target: TARGET, "Sbi `DBCN` not exist");
+                    result = false;
+                }
+                Begin => info!(target: TARGET, "Testing `DBCN`"),
+                Pass => info!(target: TARGET, "Sbi `DBCN` test pass"),
+                WriteByte => {}
+                WritingByteFailed(ret) => {
+                    error!(target: TARGET, "writing byte failed: {ret:?}");
+                    result = false;
+                }
+                WriteSlice => info!(target: TARGET, "writing slice successfuly"),
+                WritingPartialSlice(len) => {
+                    warn!(target: TARGET, "writing partial slice: {len} bytes written");
+                }
+                WritingSliceFailed(ret) => {
+                    error!(target: TARGET, "writing slice failed: {ret:?}");
+                    result = false;
+                }
+                Read(len) => info!(target: TARGET, "reading {len} bytes from console"),
+                ReadingFailed(ret) => {
+                    error!(target: TARGET, "reading failed: {ret:?}");
+                    result = false;
+                }
+            }
+        });
+        result
+    }
+}

+ 56 - 0
sbi-testing/src/spi.rs

@@ -0,0 +1,56 @@
+//! Inter-processor interrupt extension test suite.
+
+use crate::thread::Thread;
+use riscv::register::{
+    scause::Interrupt,
+    scause::{self, Trap},
+    sie,
+};
+
+/// Inter-processor Interrupt extension test cases.
+#[derive(Clone, Debug)]
+pub enum Case {
+    /// Can't procceed test for inter-processor interrupt extension does not exist.
+    NotExist,
+    /// Test begin.
+    Begin,
+    /// Test process for an inter-processor interrupt has been received.
+    SendIpi,
+    /// Test failed for unexpected trap occurred upon tests.
+    UnexpectedTrap(Trap),
+    /// All test cases on inter-processor interrupt extension has passed.
+    Pass,
+}
+
+/// Test inter-processor interrupt extension.
+pub fn test(hart_id: usize, mut f: impl FnMut(Case)) {
+    if sbi::probe_extension(sbi::Timer).is_unavailable() {
+        f(Case::NotExist);
+        return;
+    }
+
+    fn ipi(hart_id: usize) -> ! {
+        sbi::send_ipi(sbi_spec::binary::HartMask::from_mask_base(1 << hart_id, 0));
+        // 必须立即触发中断,即使是一个指令的延迟,也会触发另一个异常
+        unsafe { core::arch::asm!("unimp", options(noreturn, nomem)) };
+    }
+
+    f(Case::Begin);
+    let mut stack = [0usize; 32];
+    let mut thread = Thread::new(ipi as _);
+    *thread.sp_mut() = stack.as_mut_ptr_range().end as _;
+    *thread.a_mut(0) = hart_id;
+    unsafe {
+        sie::set_ssoft();
+        thread.execute();
+    }
+    match scause::read().cause() {
+        Trap::Interrupt(Interrupt::SupervisorSoft) => {
+            f(Case::SendIpi);
+            f(Case::Pass);
+        }
+        trap => {
+            f(Case::UnexpectedTrap(trap));
+        }
+    }
+}

+ 180 - 0
sbi-testing/src/thread.rs

@@ -0,0 +1,180 @@
+/// 线程上下文。
+#[repr(C)]
+pub struct Thread {
+    sctx: usize,
+    x: [usize; 31],
+    sepc: usize,
+}
+
+#[allow(unused)]
+impl Thread {
+    /// 创建空白上下文。
+    #[inline]
+    pub const fn new(sepc: usize) -> Self {
+        Self {
+            sctx: 0,
+            x: [0; 31],
+            sepc,
+        }
+    }
+
+    /// 读取通用寄存器。
+    #[inline]
+    pub fn x(&self, n: usize) -> usize {
+        self.x[n - 1]
+    }
+
+    /// 修改通用寄存器。
+    #[inline]
+    pub fn x_mut(&mut self, n: usize) -> &mut usize {
+        &mut self.x[n - 1]
+    }
+
+    /// 读取参数寄存器。
+    #[inline]
+    pub fn a(&self, n: usize) -> usize {
+        self.x(n + 10)
+    }
+
+    /// 修改参数寄存器。
+    #[inline]
+    pub fn a_mut(&mut self, n: usize) -> &mut usize {
+        self.x_mut(n + 10)
+    }
+
+    /// 读取栈指针。
+    #[inline]
+    pub fn sp(&self) -> usize {
+        self.x(2)
+    }
+
+    /// 修改栈指针。
+    #[inline]
+    pub fn sp_mut(&mut self) -> &mut usize {
+        self.x_mut(2)
+    }
+
+    /// 将 pc 移至下一条指令。
+    ///
+    /// # Notice
+    ///
+    /// 假设这一条指令不是压缩版本。
+    #[inline]
+    pub fn move_next(&mut self) {
+        self.sepc = self.sepc.wrapping_add(4);
+    }
+
+    /// 执行此线程,并返回 `sstatus`。
+    ///
+    /// # Safety
+    ///
+    /// 将修改 `sscratch`、`sepc`、`sstatus` 和 `stvec`。
+    #[inline]
+    pub unsafe fn execute(&mut self) -> usize {
+        // 设置线程仍在 S 态并打开中断
+        let mut sstatus: usize;
+        core::arch::asm!("csrr {}, sstatus", out(reg) sstatus);
+        const PREVILEGE_BIT: usize = 1 << 8;
+        const INTERRUPT_BIT: usize = 1 << 5;
+        sstatus |= PREVILEGE_BIT | INTERRUPT_BIT;
+        // 执行线程
+        core::arch::asm!(
+            "   csrw sscratch, {sscratch}
+                csrw sepc    , {sepc}
+                csrw sstatus , {sstatus}
+                addi sp, sp, -8
+                sd   ra, (sp)
+                call {execute_naked}
+                ld   ra, (sp)
+                addi sp, sp,  8
+                csrr {sepc}   , sepc
+                csrr {sstatus}, sstatus
+            ",
+            sscratch      = in(reg) self,
+            sepc          = inlateout(reg) self.sepc,
+            sstatus       = inlateout(reg) sstatus,
+            execute_naked = sym execute_naked,
+        );
+        sstatus
+    }
+}
+
+/// 线程切换核心部分。
+///
+/// 通用寄存器压栈,然后从预存在 `sscratch` 里的上下文指针恢复线程通用寄存器。
+///
+/// # Safety
+///
+/// 裸函数。
+#[naked]
+unsafe extern "C" fn execute_naked() {
+    core::arch::asm!(
+        r"  .altmacro
+            .macro SAVE n
+                sd x\n, \n*8(sp)
+            .endm
+            .macro SAVE_ALL
+                sd x1, 1*8(sp)
+                .set n, 3
+                .rept 29
+                    SAVE %n
+                    .set n, n+1
+                .endr
+            .endm
+
+            .macro LOAD n
+                ld x\n, \n*8(sp)
+            .endm
+            .macro LOAD_ALL
+                ld x1, 1*8(sp)
+                .set n, 3
+                .rept 29
+                    LOAD %n
+                    .set n, n+1
+                .endr
+            .endm
+        ",
+        // 位置无关加载
+        "   .option push
+            .option nopic
+        ",
+        // 保存调度上下文
+        "   addi sp, sp, -32*8
+            SAVE_ALL
+        ",
+        // 设置陷入入口
+        "   la   t0, 1f
+            csrw stvec, t0
+        ",
+        // 保存调度上下文地址并切换上下文
+        "   csrr t0, sscratch
+            sd   sp, (t0)
+            mv   sp, t0
+        ",
+        // 恢复线程上下文
+        "   LOAD_ALL
+            ld   sp, 2*8(sp)
+        ",
+        // 执行线程
+        "   sret",
+        // 陷入
+        "   .align 2",
+        // 切换上下文
+        "1: csrrw sp, sscratch, sp",
+        // 保存线程上下文
+        "   SAVE_ALL
+            csrrw t0, sscratch, sp
+            sd    t0, 2*8(sp)
+        ",
+        // 切换上下文
+        "   ld sp, (sp)",
+        // 恢复调度上下文
+        "   LOAD_ALL
+            addi sp, sp, 32*8
+        ",
+        // 返回调度
+        "   ret",
+        "   .option pop",
+        options(noreturn)
+    )
+}

+ 94 - 0
sbi-testing/src/time.rs

@@ -0,0 +1,94 @@
+//! Timer programmer extension test suite.
+
+use crate::thread::Thread;
+use riscv::register::scause::{self, Trap};
+
+/// Timer programmer extension test cases.
+#[derive(Clone, Debug)]
+pub enum Case {
+    /// Can't procceed test for Timer extension does not exist.
+    NotExist,
+    /// Test begin.
+    Begin,
+    /// Test process for time interval overhead between two reads.
+    Interval {
+        /// The time counter value for the first read.
+        begin: u64,
+        /// The time counter value for the second read.
+        end: u64,
+    },
+    /// Test failed for can't read `time` register.
+    ReadFailed,
+    /// Test failed for time counter has decreased during period of two reads.
+    TimeDecreased {
+        /// The time counter value for the first read.
+        a: u64,
+        /// The time counter value for the second read.
+        b: u64,
+    },
+    /// Test process for timer has been set.
+    SetTimer,
+    /// Test failed for unexpected trap during timer test.
+    UnexpectedTrap(Trap),
+    /// All test cases on timer extension has passed.
+    Pass,
+}
+
+/// Test timer extension.
+pub fn test(delay: u64, mut f: impl FnMut(Case)) {
+    use riscv::register::{scause::Interrupt, sie, time};
+
+    if sbi::probe_extension(sbi::Timer).is_unavailable() {
+        f(Case::NotExist);
+        return;
+    }
+    f(Case::Begin);
+    let begin: u64;
+    let end: u64;
+    let mut ok = 0xffusize;
+    unsafe {
+        core::arch::asm!(
+            "   la   {stvec}, 1f
+                csrw stvec,   {stvec}
+                csrr {begin}, time
+                csrr {end},   time
+                mv   {ok},    zero
+            .align 2
+            1:
+            ",
+            stvec = out(reg) _,
+            begin = out(reg) begin,
+            end   = out(reg) end,
+            ok    = inlateout(reg) ok,
+        );
+    }
+    if ok != 0 {
+        f(Case::ReadFailed);
+        return;
+    }
+    if begin >= end {
+        f(Case::TimeDecreased { a: begin, b: end });
+        return;
+    }
+    f(Case::Interval { begin, end });
+
+    let mut stack = [0usize; 32];
+    let mut thread = Thread::new(riscv::asm::wfi as _);
+    *thread.sp_mut() = stack.as_mut_ptr_range().end as _;
+
+    sbi::set_timer(time::read64() + delay);
+    unsafe {
+        sie::set_stimer();
+        thread.execute();
+    }
+    match scause::read().cause() {
+        Trap::Interrupt(Interrupt::SupervisorTimer) => {
+            sbi::set_timer(u64::MAX);
+            f(Case::SetTimer);
+            f(Case::Pass);
+        }
+        trap => {
+            f(Case::UnexpectedTrap(trap));
+        }
+    }
+}