From c249573f3d7c65da4aee550b1ce2d5be5831af18 Mon Sep 17 00:00:00 2001 From: Y0SH1M4S73R Date: Sun, 30 Jun 2024 22:02:07 -0400 Subject: [PATCH] some unit tests --- .cargo/config.toml | 2 + .github/workflows/rust.yml | 32 +++--- Cargo.lock | 114 ++++++++++++++++++--- Cargo.toml | 6 +- dmsrc/api.dm | 2 + tests/dm/tests.dme | 11 ++ tests/dm/util.dm | 52 ++++++++++ tests/dm/world.dm | 110 ++++++++++++++++++++ tests/tests.rs | 201 +++++++++++++++++++++++++++++++++++++ 9 files changed, 500 insertions(+), 30 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 tests/dm/tests.dme create mode 100644 tests/dm/util.dm create mode 100644 tests/dm/world.dm create mode 100644 tests/tests.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..9380186 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "i686-pc-windows-msvc" diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ea891eb..14f0015 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -69,14 +69,14 @@ jobs: env: BYOND_MAJOR: 515 BYOND_MINOR: 1640 - #PKG_CONFIG_ALLOW_CROSS: 1 + PKG_CONFIG_ALLOW_CROSS: 1 steps: - uses: actions/checkout@v4 - run: | sudo dpkg --add-architecture i386 sudo apt-get update sudo apt-get install g++-multilib zlib1g-dev:i386 libssl-dev:i386 - # ./scripts/install_byond.sh + ./scripts/install_byond.sh - uses: actions-rs/toolchain@v1 with: toolchain: stable @@ -87,20 +87,20 @@ jobs: toolchain: stable command: check args: --target i686-unknown-linux-gnu --all-features - #- name: Build (Debug) (all features) - # uses: actions-rs/cargo@v1 - # with: - # toolchain: stable - # command: build - # args: --target i686-unknown-linux-gnu --all-features - #- name: Run tests (all features) - # uses: actions-rs/cargo@v1 - # with: - # toolchain: stable - # command: test - # args: --target i686-unknown-linux-gnu --all-features - # env: - # BYOND_BIN: /home/runner/BYOND/byond/bin + - name: Build (Debug) (all features) + uses: actions-rs/cargo@v1 + with: + toolchain: stable + command: build + args: --target i686-unknown-linux-gnu --all-features + - name: Run tests (all features) + uses: actions-rs/cargo@v1 + with: + toolchain: stable + command: test + args: --target i686-unknown-linux-gnu --all-features + env: + BYOND_BIN: /home/runner/BYOND/byond/bin - name: Build (release) (default features) uses: actions-rs/cargo@v1 with: diff --git a/Cargo.lock b/Cargo.lock index 1e3af95..e8088ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,6 +122,18 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" +[[package]] +name = "cargo_metadata" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e3374c604fb39d1a2f35ed5e4a4e30e60d01fab49446e08f1b3e9a90aef202" +dependencies = [ + "semver", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "cc" version = "1.0.83" @@ -163,16 +175,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "325bfa454baa4898475be5c0ec5f3a5dd3c2e4c4a9ef5e951a25427e9928de1f" -[[package]] -name = "ctor" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e" -dependencies = [ - "quote", - "syn", -] - [[package]] name = "deranged" version = "0.3.11" @@ -196,10 +198,12 @@ name = "dreamluau" version = "0.1.0" dependencies = [ "constcat", - "ctor", "dreamluau_proc_macro", + "glob", "meowtonin", "mlua", + "portpicker", + "test-cdylib", "thiserror", ] @@ -228,6 +232,17 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.28.1" @@ -275,9 +290,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.150" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" @@ -543,12 +558,27 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "portpicker" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be97d76faf1bfab666e1375477b23fde79eccf0276e9b63b92a39d676a889ba9" +dependencies = [ + "rand", +] + [[package]] name = "powerfmt" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "prettyplease" version = "0.2.15" @@ -583,6 +613,18 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", "rand_core", ] @@ -591,6 +633,9 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] [[package]] name = "redox_syscall" @@ -667,6 +712,22 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", + "serde", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + [[package]] name = "serde" version = "1.0.193" @@ -747,6 +808,18 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "test-cdylib" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8f41b1f729f5ff5177beab62e5a9251e318df8386e260ab3c944cff502ee78d" +dependencies = [ + "cargo_metadata", + "serde", + "serde_json", + "toml", +] + [[package]] name = "thiserror" version = "1.0.50" @@ -800,6 +873,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -812,6 +894,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "which" version = "4.4.2" diff --git a/Cargo.toml b/Cargo.toml index 01c56fc..b8ccb6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,10 @@ crate-type = ["cdylib"] meowtonin = { git = "https://github.com/Absolucy/meowtonin" } mlua = { version = "0.9.5", features = ["luau", "unstable"] } thiserror = { version = "*" } -ctor = { version = "*" } constcat = { version = "*" } dreamluau_proc_macro = { path = "./proc_macro" } + +[dev-dependencies] +glob = { version = "*" } +portpicker = { version = "*" } +test-cdylib = { version = "*" } diff --git a/dmsrc/api.dm b/dmsrc/api.dm index 0216503..b4d22d9 100644 --- a/dmsrc/api.dm +++ b/dmsrc/api.dm @@ -1,4 +1,6 @@ +#ifndef DREAMLUAU #define DREAMLUAU (world.system_type == MS_WINDOWS ? "dreamluau.dll" : "libdreamluau.so") +#endif #define DREAMLUAU_CALL(func) call_ext(DREAMLUAU, "byond:[#func]") diff --git a/tests/dm/tests.dme b/tests/dm/tests.dme new file mode 100644 index 0000000..7a7b12e --- /dev/null +++ b/tests/dm/tests.dme @@ -0,0 +1,11 @@ +// BEGIN_INTERNALS +// END_INTERNALS +// BEGIN_FILE_DIR +#define FILE_DIR . +// END_FILE_DIR +// BEGIN_PREFERENCES +// END_PREFERENCES +// BEGIN_INCLUDE +#include "util.dm" +#include "world.dm" +// END_INCLUDE diff --git a/tests/dm/util.dm b/tests/dm/util.dm new file mode 100644 index 0000000..dddfd98 --- /dev/null +++ b/tests/dm/util.dm @@ -0,0 +1,52 @@ +#define ASSERT_EQ_MSG(l, r, msg) if(##l != ##r) \ +{\ + throw EXCEPTION("Assertion failed: [##msg]");\ +} + +#define ASSERT_EQ(l, r) ASSERT_EQ_MSG(l, r, "[#l] == [r]") + +#define ASSERT_FINDTEXT(value, needle, msg) if(!findtext(needle, value)) \ +{\ + throw EXCEPTION("Assertion failed: [##msg]");\ +} + +/proc/deep_compare_list(list/list_1, list/list_2, index_name = "") + if(list_1 == list_2) + return TRUE + + if(!islist(list_1) || !islist(list_2)) + return FALSE + + if(list_1.len != list_2.len) + return FALSE + + for(var/i in 1 to list_1.len) + var/key_1 = list_1[i] + var/key_2 = list_2[i] + if (islist(key_1) && islist(key_2)) + deep_compare_list(key_1, key_2, "[index_name]\[[i]\]") + else + ASSERT_EQ_MSG(key_1, key_2, "[index_name]\[[i]\] == [key_2]") + if(istext(key_1) || islist(key_1) || ispath(key_1) || istype(key_1, /datum) || key_1 == world) + var/value_1 = list_1[key_1] + var/value_2 = list_2[key_1] + if (islist(value_1) && islist(value_2)) + deep_compare_list(value_1, value_2, "[index_name]\[[key_1]\]") + else + ASSERT_EQ_MSG(value_1, value_2, "[index_name]\[[key_1]\] == [value_2]") + return TRUE + +/proc/assert_result(result, status, values, variants, errmsg) + if(istext(result)) + throw EXCEPTION(result) + ASSERT(islist(result)) + if(status) + ASSERT_EQ_MSG(result["status"], status, "expexted status of \"[status]\", got \"[result["status"]]\"") + if(status == "error") + ASSERT_FINDTEXT(result["message"], errmsg, "expected error message containing \"[errmsg]\", got \"[result["message"]]\"") + if(isnum(values)) + ASSERT_EQ(length(result["return_values"]), values) + else if(islist(values)) + deep_compare_list(result["return_values"], values, "return values") + if(islist(variants)) + deep_compare_list(result["variants"], variants, "variants") \ No newline at end of file diff --git a/tests/dm/world.dm b/tests/dm/world.dm new file mode 100644 index 0000000..0a7cee4 --- /dev/null +++ b/tests/dm/world.dm @@ -0,0 +1,110 @@ +#include "../../dmsrc/api.dm" + +/proc/print(_state_id, list/arguments) + world.log << arguments.Join(" ") + +/world/Topic(T, address) + var/list/params = params2list(T) + var/test_to_run = params["test"] + if(test_to_run) + try + call("/proc/[test_to_run]")() + catch(var/exception/e) + return "[e]" + +#define TEST(name, body, cleanup...) /proc/##name() \ +{ \ + var/state = DREAMLUAU_NEW_STATE(); \ + var/error; \ + try \ + { \ + ##body \ + } \ + catch(var/e) \ + { \ + error = e; \ + } \ + ##cleanup \ + DREAMLUAU_KILL_STATE(state);\ + if(error) \ + { \ + throw e; \ + } \ +} + +TEST(hello_world, + DREAMLUAU_SET_PRINT_WRAPPER("/proc/print");\ + var/result = DREAMLUAU_LOAD(state, "print(\"Hello World!\")");\ + assert_result(result, "finished", 0), + DREAMLUAU_SET_PRINT_WRAPPER(null);) + +TEST(usr_pushing, + var/mob/M = new();\ + DREAMLUAU_CALL(set_usr)(M);\ + var/result_1 = DREAMLUAU_LOAD(state, "return dm.usr");\ + assert_result(result_1, "finished", list(M));\ + var/result_2 = DREAMLUAU_LOAD(state, "return dm.usr");\ + assert_result(result_2, "finished", list(null))) + +TEST(calling, + var/result_1 = DREAMLUAU_LOAD(state, "function foo() return \"foo\" end");\ + assert_result(result_1, "finished", 0);\ + var/result_2 = DREAMLUAU_CALL_FUNCTION(state, list("foo"), list());\ + assert_result(result_2, "finished", list("foo"))) + +TEST(sleeping, + var/result_1 = DREAMLUAU_LOAD(state, "sleep() return \"foo\"");\ + assert_result(result_1, "sleep");\ + var/result_2 = DREAMLUAU_LOAD(state, "sleep() return \"bar\"");\ + assert_result(result_2, "sleep");\ + var/result_3 = DREAMLUAU_AWAKEN(state);\ + assert_result(result_3, "finished", list("foo"));\ + var/result_4 = DREAMLUAU_AWAKEN(state);\ + assert_result(result_4, "finished", list("bar"))) + +TEST(yielding, + var/result_1 = DREAMLUAU_LOAD(state, "return coroutine.yield(\"foo\")");\ + assert_result(result_1, "yield", list("foo"));\ + var/result_2 = DREAMLUAU_LOAD(state, "return coroutine.yield(\"bar\")");\ + assert_result(result_2, "yield", list("bar"));\ + var/result_3 = DREAMLUAU_RESUME(state, 1, list("baz"));\ + assert_result(result_3, "finished", list("baz"));\ + var/result_4 = DREAMLUAU_RESUME(state, 0, list("quux"));\ + assert_result(result_4, "finished", list("quux"))) + +/proc/get_wrapper() + return "bar" + +TEST(reading, + var/obj/O = new();\ + O.name = "foo";\ + var/result_1 = DREAMLUAU_LOAD(state, "function read(thing, var) return thing\[var\] end");\ + assert_result(result_1, "finished", 0);\ + var/result_2 = DREAMLUAU_CALL_FUNCTION(state, list("read"), list(O, "name"));\ + assert_result(result_2, "finished", list("foo"));\ + DREAMLUAU_SET_VAR_GET_WRAPPER("get_wrapper");\ + var/result_3 = DREAMLUAU_CALL_FUNCTION(state, list("read"), list(O, "name"));\ + assert_result(result_3, "finished", list("bar")), + DREAMLUAU_SET_VAR_GET_WRAPPER(null);) + +/proc/set_wrapper(datum/D, var_name) + D.vars[var_name] = "baz" + +TEST(writing, + var/obj/O = new();\ + var/result_1 = DREAMLUAU_LOAD(state, "function write(thing, var, value) thing\[var\] = value end");\ + assert_result(result_1, "finished", 0);\ + var/result_2 = DREAMLUAU_CALL_FUNCTION(state, list("write"), list(O, "name", "foo"));\ + assert_result(result_2, "finished", 0);\ + ASSERT_EQ(O.name, "foo");\ + DREAMLUAU_SET_VAR_SET_WRAPPER("set_wrapper");\ + var/result_3 = DREAMLUAU_CALL_FUNCTION(state, list("write"), list(O, "name", "bar"));\ + assert_result(result_3, "finished", 0);\ + ASSERT_EQ(O.name, "baz"), + DREAMLUAU_SET_VAR_SET_WRAPPER(null);) + +TEST(variants, + var/result_1 = DREAMLUAU_LOAD(state, "return function() end, coroutine.create(function() end)");\ + assert_result(result_1, "finished", variants = list("function", "thread"));\ + var/result_2 = DREAMLUAU_LOAD(state, "return {\"foo\", \"bar\", \"baz\"}");\ + assert_result(result_2, "finished", list("foo", "bar", "baz"))) \ No newline at end of file diff --git a/tests/tests.rs b/tests/tests.rs new file mode 100644 index 0000000..cd65bd6 --- /dev/null +++ b/tests/tests.rs @@ -0,0 +1,201 @@ +#![allow(unused_must_use)] + +use glob::glob; +use portpicker::pick_unused_port; +use std::{ + borrow::Cow, + ffi::CStr, + fmt::Display, + fs, + io::{Error, ErrorKind, Read, Result as IoResult, Write}, + net::TcpStream, + ops::DerefMut, + path::{PathBuf, MAIN_SEPARATOR_STR}, + process::{Child, Command}, + sync::{Once, OnceLock, RwLock, RwLockWriteGuard}, +}; + +fn byond_bin() -> PathBuf { + PathBuf::from(std::env::var("BYOND_BIN").expect("environment variable BYOND_BIN")) +} + +fn build() { + let byond_bin = byond_bin(); + let dylib_path = test_cdylib::build_current_project() + .to_str() + .expect("target path is invalid UTF-8") + .replace(MAIN_SEPARATOR_STR, "/"); + let build_status = if cfg!(windows) { + Command::new(byond_bin.join("dm.exe")) + } else { + let mut cmd = Command::new(byond_bin.join("byondexec")); + cmd.arg(byond_bin.join("DreamMaker")); + cmd + } + .arg(format!("-DDREAMLUAU=\"{dylib_path}\"")) + .arg(PathBuf::from_iter(["tests", "dm", "tests.dme"])) + .status() + .unwrap(); + if !build_status.success() { + panic!("process exited with {:?}", build_status); + } +} + +fn free_port() -> &'static u16 { + static FREE_PORT: OnceLock = OnceLock::new(); + FREE_PORT.get_or_init(|| pick_unused_port().expect("No open ports")) +} + +fn topic_stream() -> RwLockWriteGuard<'static, TcpStream> { + static TOPIC_STREAM: OnceLock> = OnceLock::new(); + TOPIC_STREAM + .get_or_init(|| { + let stream = TcpStream::connect(("127.0.0.1", *free_port())).unwrap(); + RwLock::new(stream) + }) + .write() + .unwrap() +} +#[derive(Clone, Debug, PartialEq)] +enum TopicResponse { + Null, + Float(f32), + String(String), +} + +impl Display for TopicResponse { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Null => f.write_str("null"), + Self::Float(fl) => f.write_fmt(format_args!("{fl}")), + Self::String(s) => f.write_str(s), + } + } +} + +fn send_topic>( + stream: &mut S, + topic: &str, +) -> IoResult { + let len = topic.len() + 6; + if len > u16::MAX as usize { + return Err(Error::new(ErrorKind::Other, "payload size too large")); + } + stream.write_all( + [0x00, 0x83] + .into_iter() + .chain((len as u16).to_be_bytes()) + .chain([0x00; 5]) + .chain(topic.bytes()) + .chain([0x00]) + .collect::>() + .as_slice(), + )?; + stream.flush()?; + let mut response = [0; 65535]; + let bytes_read = stream.read(&mut response[..])?; + if bytes_read < 5 { + return Err(Error::other("response too small")); + } + match response[4] { + 0x00 => Ok(TopicResponse::Null), + 0x2A => { + if bytes_read > 9 { + let mut float_bytes: [u8; 4] = Default::default(); + float_bytes.clone_from_slice(&response[5..9]); + Ok(TopicResponse::Float(f32::from_be_bytes(float_bytes))) + } else { + Err(Error::other("response too small")) + } + } + 0x06 => { + let mut length_bytes: [u8; 2] = Default::default(); + length_bytes.clone_from_slice(&response[2..4]); + let string_length = (u16::from_be_bytes(length_bytes) - 1) as usize; + if bytes_read < string_length + 5 { + Err(Error::other("response too small")) + } else { + CStr::from_bytes_with_nul(&response[5..(5 + string_length)]) + .map_err(Error::other) + .map(CStr::to_string_lossy) + .map(Cow::into_owned) + .map(TopicResponse::String) + } + } + n => Err(Error::other(format! {"unexpected response type {n}"})), + } +} + +fn dreamdaemon() -> RwLockWriteGuard<'static, Child> { + static DREAMDAEMON: OnceLock> = OnceLock::new(); + static EXIT: Once = Once::new(); + EXIT.call_once(|| unsafe { + extern "C" { + fn atexit(cb: unsafe extern "C" fn()); + } + extern "C" fn cleanup() { + if let Some(Ok(mut dreamdaemon)) = DREAMDAEMON.get().map(RwLock::write) { + dreamdaemon.kill(); + } + #[cfg(debug_assertions)] + for logfile in glob("tests/dm/meowtonin*.log").unwrap() { + if let Ok(file) = logfile { + fs::remove_file(file); + } + } + } + atexit(cleanup); + }); + DREAMDAEMON + .get_or_init(|| { + build(); + let byond_bin = byond_bin(); + RwLock::new( + if cfg!(windows) { + Command::new(byond_bin.join("dd.exe")) + } else { + let mut cmd = Command::new(byond_bin.join("byondexec")); + cmd.arg(byond_bin.join("DreamDaemon")); + cmd + } + .arg(PathBuf::from_iter(["tests", "dm", "tests.dmb"])) + .arg(free_port().to_string()) + .arg("-trusted") + .arg("-invisible") + .spawn() + .unwrap(), + ) + }) + .write() + .unwrap() +} + +macro_rules! simple_test { + ($name:ident) => { + #[test] + fn $name() { + dreamdaemon(); + let response = send_topic( + &mut topic_stream(), + format!("?test={}", stringify!($name)).as_str(), + ); + assert_eq!(response.unwrap(), TopicResponse::Null) + } + }; +} + +simple_test!(hello_world); + +simple_test!(usr_pushing); + +simple_test!(calling); + +simple_test!(sleeping); + +simple_test!(yielding); + +simple_test!(reading); + +simple_test!(writing); + +simple_test!(variants);