Add some tests

This commit is contained in:
Jasper Bekkers 2020-09-03 16:51:23 +01:00
parent 1f4a2d0eb0
commit 733008a993
No known key found for this signature in database
GPG Key ID: C59CE25F4DA6625D
4 changed files with 533 additions and 30 deletions

233
rspirv-linker/Cargo.lock generated
View File

@ -9,6 +9,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "ansi_term"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
dependencies = [
"winapi",
]
[[package]]
name = "autocfg"
version = "1.0.1"
@ -27,6 +36,22 @@ version = "1.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "ctor"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39858aa5bac06462d4dd4b9164848eb81ffc4aa5c479746393598fd193afa227"
dependencies = [
"quote 1.0.7",
"syn 1.0.39",
]
[[package]]
name = "derive_more"
version = "0.15.0"
@ -34,13 +59,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a141330240c921ec6d074a3e188a7c7ef95668bb95e7d44fa0e5778ec2a7afe"
dependencies = [
"lazy_static",
"proc-macro2",
"quote",
"proc-macro2 0.4.30",
"quote 0.6.13",
"regex",
"rustc_version",
"syn",
"syn 0.15.44",
]
[[package]]
name = "difference"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
[[package]]
name = "fxhash"
version = "0.2.1"
@ -50,12 +81,29 @@ dependencies = [
"byteorder",
]
[[package]]
name = "getrandom"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "755456fae044e6fa1ebbbd1b3e902ae19e73097ed4ed87bb79934a867c007bc3"
[[package]]
name = "memchr"
version = "2.3.3"
@ -71,13 +119,49 @@ dependencies = [
"autocfg",
]
[[package]]
name = "output_vt100"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9"
dependencies = [
"winapi",
]
[[package]]
name = "ppv-lite86"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20"
[[package]]
name = "pretty_assertions"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427"
dependencies = [
"ansi_term",
"ctor",
"difference",
"output_vt100",
]
[[package]]
name = "proc-macro2"
version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
dependencies = [
"unicode-xid",
"unicode-xid 0.1.0",
]
[[package]]
name = "proc-macro2"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12"
dependencies = [
"unicode-xid 0.2.1",
]
[[package]]
@ -86,9 +170,65 @@ version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
dependencies = [
"proc-macro2",
"proc-macro2 0.4.30",
]
[[package]]
name = "quote"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
dependencies = [
"proc-macro2 1.0.19",
]
[[package]]
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [
"getrandom",
"libc",
"rand_chacha",
"rand_core",
"rand_hc",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [
"getrandom",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
dependencies = [
"rand_core",
]
[[package]]
name = "redox_syscall"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]]
name = "regex"
version = "1.3.9"
@ -107,6 +247,15 @@ version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8"
[[package]]
name = "remove_dir_all"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
dependencies = [
"winapi",
]
[[package]]
name = "rspirv"
version = "0.7.0"
@ -121,7 +270,10 @@ dependencies = [
name = "rspirv-linker"
version = "0.1.0"
dependencies = [
"pretty_assertions",
"rspirv",
"tempfile",
"topological-sort",
]
[[package]]
@ -162,9 +314,34 @@ version = "0.15.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
"proc-macro2 0.4.30",
"quote 0.6.13",
"unicode-xid 0.1.0",
]
[[package]]
name = "syn"
version = "1.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891d8d6567fe7c7f8835a3a98af4208f3846fba258c1bc3c31d6e506239f11f9"
dependencies = [
"proc-macro2 1.0.19",
"quote 1.0.7",
"unicode-xid 0.2.1",
]
[[package]]
name = "tempfile"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
dependencies = [
"cfg-if",
"libc",
"rand",
"redox_syscall",
"remove_dir_all",
"winapi",
]
[[package]]
@ -176,8 +353,48 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "topological-sort"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa7c7f42dea4b1b99439786f5633aeb9c14c1b53f75e282803c2ec2ad545873c"
[[package]]
name = "unicode-xid"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
[[package]]
name = "unicode-xid"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

View File

@ -7,4 +7,9 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rspirv = { path = "C:/Users/Jasper/traverse/rspirv/rspirv/"}
rspirv = { path = "C:/Users/Jasper/traverse/rspirv/rspirv/"}
topological-sort = "0.1"
[dev-dependencies]
tempfile = "3.1"
pretty_assertions = "0.6"

View File

@ -1,8 +1,10 @@
use rspirv::binary::Assemble;
mod test;
use rspirv::binary::Consumer;
use rspirv::binary::Disassemble;
use rspirv::spirv;
use std::collections::{HashMap, HashSet};
use topological_sort::TopologicalSort;
fn load(bytes: &[u8]) -> rspirv::dr::Module {
let mut loader = rspirv::dr::Loader::new();
@ -104,6 +106,10 @@ fn kill_with<F>(insts: &mut Vec<rspirv::dr::Instruction>, f: F)
where
F: Fn(&rspirv::dr::Instruction) -> bool,
{
if insts.is_empty() {
return;
}
let mut idx = insts.len() - 1;
// odd backwards loop so we can swap_remove
loop {
@ -283,7 +289,7 @@ impl LinkInfo {
}
/// returns the list of matching import / export pairs after validation the list of potential pairs
fn ensure_matching_import_export_pairs(&self, defs: &DefAnalyzer) -> &Vec<ImportExportPair> {
fn ensure_matching_import_export_pairs(&self) -> &Vec<ImportExportPair> {
for pair in &self.potential_pairs {
for (import_param, export_param) in pair
.import
@ -339,7 +345,28 @@ fn import_kill_annotations_and_debug(module: &mut rspirv::dr::Module, info: &Lin
}
}
fn kill_linkage_instructions(pairs: &Vec<ImportExportPair>, module: &mut rspirv::dr::Module) {
struct Options {
/// `true` if we're creating a library
lib: bool,
/// `true` if partial linking is allowed
partial: bool,
}
impl Default for Options {
fn default() -> Self {
Self {
lib: false,
partial: false,
}
}
}
fn kill_linkage_instructions(
pairs: &Vec<ImportExportPair>,
module: &mut rspirv::dr::Module,
opts: &Options,
) {
// drop imported functions
for pair in pairs.iter() {
module
@ -372,6 +399,15 @@ fn kill_linkage_instructions(pairs: &Vec<ImportExportPair>, module: &mut rspirv:
== rspirv::dr::Operand::Decoration(spirv::Decoration::LinkageAttributes)
});
if !opts.lib {
kill_with(&mut module.annotations, |inst| {
inst.class.opcode == spirv::Op::Decorate
&& inst.operands[1]
== rspirv::dr::Operand::Decoration(spirv::Decoration::LinkageAttributes)
&& inst.operands[3] == rspirv::dr::Operand::LinkageType(spirv::LinkageType::Export)
});
}
// drop OpCapability Linkage
kill_with(&mut module.capabilities, |inst| {
inst.class.opcode == spirv::Op::Capability
@ -414,8 +450,52 @@ fn compact_ids(module: &mut rspirv::dr::Module) -> u32 {
remap.len() as u32 + 1
}
fn link(inputs: &mut [&mut rspirv::dr::Module]) -> rspirv::dr::Module {
// 1. shift all the ids
fn sort_globals(module: &mut rspirv::dr::Module) {
let mut ts = TopologicalSort::<u32>::new();
for t in module.types_global_values.iter() {
if let Some(result_type) = t.result_type {
if let Some(result_id) = t.result_id {
ts.add_dependency(result_type, result_id);
for op in &t.operands {
match op {
rspirv::dr::Operand::IdMemorySemantics(w)
| rspirv::dr::Operand::IdScope(w)
| rspirv::dr::Operand::IdRef(w) => {
ts.add_dependency(*w, result_id); // the op defining the IdRef should come before our op / result_id
}
_ => {}
}
}
}
}
}
let defs = DefAnalyzer::new(&module);
let mut new_types_global_values = vec![];
loop {
let mut v = ts.pop_all();
v.sort();
for result_id in v {
new_types_global_values.push(defs.def(result_id).unwrap().clone());
}
if ts.is_empty() {
break;
}
}
assert!(module.types_global_values.len() == new_types_global_values.len());
module.types_global_values = new_types_global_values;
}
fn link(inputs: &mut [&mut rspirv::dr::Module], opts: &Options) -> rspirv::dr::Module {
// shift all the ids
let mut bound = inputs[0].header.as_ref().unwrap().bound - 1;
for mut module in inputs.iter_mut().skip(1) {
@ -423,11 +503,11 @@ fn link(inputs: &mut [&mut rspirv::dr::Module]) -> rspirv::dr::Module {
bound += module.header.as_ref().unwrap().bound - 1;
}
println!("{}\n\n", inputs[0].disassemble());
println!("{}\n\n", inputs[1].disassemble());
for i in inputs.iter() {
println!("{}\n\n", i.disassemble());
}
// 2. generate the header (todo)
// 3. merge the binaries
// merge the binaries
let mut loader = rspirv::dr::Loader::new();
for module in inputs.iter() {
@ -438,33 +518,45 @@ fn link(inputs: &mut [&mut rspirv::dr::Module]) -> rspirv::dr::Module {
let mut output = loader.module();
// 4. find import / export pairs
// find import / export pairs
let defs = DefAnalyzer::new(&output);
let info = find_import_export_pairs(&output, &defs);
// 5. ensure import / export pairs have matching types and defintions
let matching_pairs = info.ensure_matching_import_export_pairs(&defs);
// ensure import / export pairs have matching types and defintions
let matching_pairs = info.ensure_matching_import_export_pairs();
// 6. remove duplicates (https://github.com/KhronosGroup/SPIRV-Tools/blob/e7866de4b1dc2a7e8672867caeb0bdca49f458d3/source/opt/remove_duplicates_pass.cpp)
// remove duplicates (https://github.com/KhronosGroup/SPIRV-Tools/blob/e7866de4b1dc2a7e8672867caeb0bdca49f458d3/source/opt/remove_duplicates_pass.cpp)
remove_duplicates(&mut output);
// 7. remove names and decorations of import variables / functions https://github.com/KhronosGroup/SPIRV-Tools/blob/8a0ebd40f86d1f18ad42ea96c6ac53915076c3c7/source/opt/ir_context.cpp#L404
// remove names and decorations of import variables / functions https://github.com/KhronosGroup/SPIRV-Tools/blob/8a0ebd40f86d1f18ad42ea96c6ac53915076c3c7/source/opt/ir_context.cpp#L404
import_kill_annotations_and_debug(&mut output, &info);
// 8. rematch import variables and functions to export variables / functions https://github.com/KhronosGroup/SPIRV-Tools/blob/8a0ebd40f86d1f18ad42ea96c6ac53915076c3c7/source/opt/ir_context.cpp#L255
// rematch import variables and functions to export variables / functions https://github.com/KhronosGroup/SPIRV-Tools/blob/8a0ebd40f86d1f18ad42ea96c6ac53915076c3c7/source/opt/ir_context.cpp#L255
for pair in matching_pairs {
replace_all_uses_with(&mut output, pair.import.id, pair.export.id);
}
// 9. remove linkage specific instructions
kill_linkage_instructions(&matching_pairs, &mut output);
// remove linkage specific instructions
kill_linkage_instructions(&matching_pairs, &mut output, &opts);
// 10. compact the ids https://github.com/KhronosGroup/SPIRV-Tools/blob/e02f178a716b0c3c803ce31b9df4088596537872/source/opt/compact_ids_pass.cpp#L43
sort_globals(&mut output);
// compact the ids https://github.com/KhronosGroup/SPIRV-Tools/blob/e02f178a716b0c3c803ce31b9df4088596537872/source/opt/compact_ids_pass.cpp#L43
let bound = compact_ids(&mut output);
output.header = Some(rspirv::dr::ModuleHeader::new(bound));
// 11. output the module
output.debugs.push(rspirv::dr::Instruction::new(
spirv::Op::ModuleProcessed,
None,
None,
vec![rspirv::dr::Operand::LiteralString(
"Linked by rspirv-linker".to_string(),
)],
));
println!("{}\n\n", output.disassemble());
// output the module
output
}
@ -475,6 +567,11 @@ fn main() {
let mut body1 = load(&body1[..]);
let mut body2 = load(&body2[..]);
let output = link(&mut [&mut body1, &mut body2]);
let opts = Options {
lib: false,
partial: false,
};
let output = link(&mut [&mut body1, &mut body2], &opts);
println!("{}\n\n", output.disassemble());
}

184
rspirv-linker/src/test.rs Normal file
View File

@ -0,0 +1,184 @@
use crate::link;
// https://github.com/colin-kiegel/rust-pretty-assertions/issues/24
#[derive(PartialEq, Eq)]
#[doc(hidden)]
pub struct PrettyString<'a>(pub &'a str);
/// Make diff to display string as multi-line string
impl<'a> std::fmt::Debug for PrettyString<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str(self.0)
}
}
fn assemble_spirv(spirv: &str) -> Vec<u8> {
use std::process::Command;
use tempfile::tempdir;
let temp = tempdir().expect("Unable to create temp dir");
let input = temp.path().join("code.txt");
let output = temp.path().join("code.spv");
std::fs::write(&input, spirv).unwrap();
let process = Command::new("spirv-as.exe")
.arg(input.to_str().unwrap())
.arg("-o")
.arg(output.to_str().unwrap())
.output()
.expect("failed to execute process");
println!("status: {}", process.status);
println!("stdout: {}", String::from_utf8_lossy(&process.stdout));
println!("stderr: {}", String::from_utf8_lossy(&process.stderr));
assert!(process.status.success());
std::fs::read(&output).unwrap()
}
#[allow(unused)]
fn validate(spirv: &[u32]) {
use std::process::Command;
use tempfile::tempdir;
let temp = tempdir().expect("Unable to create temp dir");
let input = temp.path().join("code.spv");
let spirv = unsafe { std::slice::from_raw_parts(spirv.as_ptr() as *const u8, spirv.len() * 4) };
std::fs::write(&input, spirv).unwrap();
let process = Command::new("spirv-val.exe")
.arg(input.to_str().unwrap())
.output()
.expect("failed to execute process");
println!("status: {}", process.status);
println!("stdout: {}", String::from_utf8_lossy(&process.stdout));
println!("stderr: {}", String::from_utf8_lossy(&process.stderr));
assert!(process.status.success());
}
fn load(bytes: &[u8]) -> rspirv::dr::Module {
let mut loader = rspirv::dr::Loader::new();
rspirv::binary::parse_bytes(&bytes, &mut loader).unwrap();
let module = loader.module();
module
}
fn assemble_and_link(binaries: &[&[u8]], opts: &crate::Options) -> rspirv::dr::Module {
let mut modules = binaries.iter().cloned().map(load).collect::<Vec<_>>();
let mut modules = modules.iter_mut().map(|m| m).collect::<Vec<_>>();
link(&mut modules, opts)
}
fn without_header_eq(mut result: rspirv::dr::Module, expected: &str) {
use rspirv::binary::Disassemble;
//use rspirv::binary::Assemble;
// validate(&result.assemble());
result.header = None;
let result = result.disassemble();
let expected = expected
.split("\n")
.map(|l| l.trim())
.collect::<Vec<_>>()
.join("\n");
let result = result
.split("\n")
.map(|l| l.trim().replace(" ", " ")) // rspirv outputs multiple spaces between operands
.collect::<Vec<_>>()
.join("\n");
if result != expected {
panic!(
"assertion failed: `(left.contains(right))`\
\n\
\n{}\
\n",
pretty_assertions::Comparison::new(&PrettyString(&result), &PrettyString(&expected))
)
}
}
mod test {
use crate::test::assemble_and_link;
use crate::test::assemble_spirv;
use crate::test::without_header_eq;
use crate::Options;
#[test]
fn standard() {
let a = assemble_spirv(
r#"OpCapability Linkage
OpDecorate %1 LinkageAttributes "foo" Import
%2 = OpTypeFloat 32
%1 = OpVariable %2 Uniform
%3 = OpVariable %2 Input"#,
);
let b = assemble_spirv(
r#"OpCapability Linkage
OpDecorate %1 LinkageAttributes "foo" Export
%2 = OpTypeFloat 32
%3 = OpConstant %2 42
%1 = OpVariable %2 Uniform %3
"#,
);
let result = assemble_and_link(&[&a, &b], &Options::default());
let expect = r#"OpModuleProcessed "Linked by rspirv-linker"
%1 = OpTypeFloat 32
%2 = OpVariable %1 Input
%3 = OpConstant %1 42.0
%4 = OpVariable %1 Uniform %3"#;
without_header_eq(result, expect);
}
#[test]
fn not_a_lib_extra_exports() {
let a = assemble_spirv(
r#"OpCapability Linkage
OpDecorate %1 LinkageAttributes "foo" Export
%2 = OpTypeFloat 32
%1 = OpVariable %2 Uniform"#,
);
let result = assemble_and_link(&[&a], &Options::default());
let expect = r#"OpModuleProcessed "Linked by rspirv-linker"
%1 = OpTypeFloat 32
%2 = OpVariable %1 Uniform"#;
without_header_eq(result, expect);
}
#[test]
fn lib_extra_exports() {
let a = assemble_spirv(
r#"OpCapability Linkage
OpDecorate %1 LinkageAttributes "foo" Export
%2 = OpTypeFloat 32
%1 = OpVariable %2 Uniform"#,
);
let result = assemble_and_link(
&[&a],
&Options {
lib: true,
..Default::default()
},
);
let expect = r#"OpModuleProcessed "Linked by rspirv-linker"
OpDecorate %1 LinkageAttributes "foo" Export
%2 = OpTypeFloat 32
%1 = OpVariable %2 Uniform"#;
without_header_eq(result, expect);
}
}