diff --git a/.github/workflows/testsuite_preview.yaml b/.github/workflows/testsuite_preview.yaml new file mode 100644 index 0000000000..0fd2ab00a6 --- /dev/null +++ b/.github/workflows/testsuite_preview.yaml @@ -0,0 +1,102 @@ +name: Testsuite Preview + +on: + - pull_request_target + +permissions: + pull-requests: write + contents: read + +jobs: + generate-base-report: + strategy: + matrix: + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + env: + RUSTFLAGS: "-D warnings" + RUSTDOCFLAGS: "-D warnings" + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.base.ref }} + submodules: true + repository: ${{ github.event.pull_request.base.repo.full_name }} + + - name: Generate old report + run: | + TESTSUITE_SAVE=1 cargo test -- spec_tests --show-output + cp testsuite_results.json old.json || : + + - name: Upload Base Report + uses: actions/upload-artifact@v4 + with: + name: base-report + path: old.json + retention-days: 1 + + - run: cargo clean + + generate-head-report: + strategy: + matrix: + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + env: + RUSTFLAGS: "-D warnings" + RUSTDOCFLAGS: "-D warnings" + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.ref }} + submodules: true + repository: ${{ github.event.pull_request.head.repo.full_name }} + + - name: Generate new report + run: | + TESTSUITE_SAVE=1 cargo test -- spec_tests --show-output + cp testsuite_results.json new.json || : + + - name: Upload Head Report + uses: actions/upload-artifact@v4 + with: + name: head-report + path: new.json + retention-days: 1 + + - run: cargo clean + + compare-reports: + needs: [generate-base-report, generate-head-report] + strategy: + matrix: + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Download Base Report + continue-on-error: true + uses: actions/download-artifact@v4 + with: + name: base-report + + - name: Download Head Report + continue-on-error: true + uses: actions/download-artifact@v4 + with: + name: head-report + + - name: Compare reports + run: python3 ./ci-tools/compare_testsuite.py old.json new.json > testsuite_report.md + + - name: Sticky Pull Request Comment + uses: marocchino/sticky-pull-request-comment@v2.9.1 + with: + header: testsuite + path: testsuite_report.md diff --git a/.gitignore b/.gitignore index 637468b741..27354775fc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /target result whitepaper/main.pdf +testsuite_results.json diff --git a/Cargo.lock b/Cargo.lock index 4a0ea52408..eeaa377275 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -17,29 +17,73 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys", +] [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "bumpalo" -version = "3.15.3" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "cast" @@ -82,18 +126,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.7" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db83dced34638ad474f39f250d7fea9598bdd239eaced1bdf45d597da0f433f" +checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.7" +version = "4.5.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7e204572485eb3fbf28f871612191521df159bc3e15a9f5064c66dba3a8c05f" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" dependencies = [ "anstyle", "clap_lex", @@ -101,9 +145,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.1" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "criterion" @@ -143,9 +193,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -162,27 +212,27 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" [[package]] name = "either" -version = "1.10.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "env_filter" -version = "0.1.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", ] @@ -202,10 +252,12 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.2" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c012a26a7f605efc424dd53697843a72be7dc86ad2d01f7814337794a12231d" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" dependencies = [ + "anstream", + "anstyle", "env_filter", "log", ] @@ -228,15 +280,15 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "hermit-abi" -version = "0.3.6" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd5256b483761cd23699d0da46cc6fd2ee3be420bbe6d020ae4a091e70b7e9fd" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "hexf" @@ -262,9 +314,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "indexmap" -version = "2.2.3" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown", @@ -272,15 +324,21 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.12" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" dependencies = [ "hermit-abi", "libc", "windows-sys", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.10.5" @@ -301,25 +359,38 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "leb128" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.169" @@ -328,9 +399,9 @@ checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "log" @@ -338,11 +409,30 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] [[package]] name = "num-traits" @@ -355,21 +445,33 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] name = "oorandom" -version = "11.1.3" +version = "11.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "plotters" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" dependencies = [ "num-traits", "plotters-backend", @@ -380,33 +482,33 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" [[package]] name = "plotters-svg" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" dependencies = [ "plotters-backend", ] [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.35" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -433,38 +535,59 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] name = "regex-automata" -version = "0.4.5" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.5", ] [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" [[package]] name = "same-file" @@ -477,41 +600,51 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.22" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" [[package]] name = "serde" -version = "1.0.203" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.98", ] [[package]] name = "serde_json" -version = "1.0.118" +version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "syn" version = "1.0.109" @@ -525,9 +658,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.52" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", @@ -545,23 +678,34 @@ dependencies = [ [[package]] name = "test-log" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b319995299c65d522680decf80f2c108d85b861d81dfe340a10d16cee29d9e6" +checksum = "e7f46083d221181166e5b6f6b1e5f1d499f3a76888826e6cb1d057554157cd0f" dependencies = [ - "env_logger 0.11.2", + "env_logger 0.11.6", "test-log-macros", + "tracing-subscriber", ] [[package]] name = "test-log-macros" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8f546451eaa38373f549093fe9fd05e7d2bade739e2ddf834b9968621d60107" +checksum = "888d0c3c6db53c0fdab160d2ed5e12ba745383d3e85813f2ea0f2b1475ab553f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.98", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", ] [[package]] @@ -574,17 +718,83 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" + +[[package]] +name = "unicode-width" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "walkdir" @@ -598,34 +808,35 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", + "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.98", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -633,39 +844,43 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.98", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "wasm-encoder" -version = "0.200.0" +version = "0.212.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e3fb0c8fbddd78aa6095b850dfeedbc7506cf5f81e633f69cf8f2333ab84b9" +checksum = "501940df4418b8929eb6d52f1aade1fdd15a5b86c92453cb696e3c906bd3fc33" dependencies = [ "leb128", ] [[package]] name = "wasm-encoder" -version = "0.212.0" +version = "0.225.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501940df4418b8929eb6d52f1aade1fdd15a5b86c92453cb696e3c906bd3fc33" +checksum = "6f7eac0445cac73bcf09e6a97f83248d64356dccf9f2b100199769b6b42464e5" dependencies = [ - "leb128", + "leb128fmt", + "wasmparser 0.225.0", ] [[package]] @@ -678,8 +893,10 @@ dependencies = [ "itertools 0.12.1", "libm", "log", + "serde", + "serde_json", "test-log", - "wasmparser", + "wasmparser 0.119.0", "wast 212.0.0", "wat", ] @@ -696,16 +913,14 @@ dependencies = [ ] [[package]] -name = "wast" -version = "200.0.0" +name = "wasmparser" +version = "0.225.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1810d14e6b03ebb8fb05eef4009ad5749c989b65197d83bce7de7172ed91366" +checksum = "36e5456165f81e64cb9908a0fe9b9d852c2c74582aa3fe2be3c2da57f937d3ae" dependencies = [ - "bumpalo", - "leb128", - "memchr", - "unicode-width", - "wasm-encoder 0.200.0", + "bitflags", + "indexmap", + "semver", ] [[package]] @@ -717,24 +932,37 @@ dependencies = [ "bumpalo", "leb128", "memchr", - "unicode-width", + "unicode-width 0.1.14", "wasm-encoder 0.212.0", ] +[[package]] +name = "wast" +version = "225.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61496027ff707f9fa9e0b22c34ec163eb7adb1070df565e32a9180a76e4300b" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width 0.2.0", + "wasm-encoder 0.225.0", +] + [[package]] name = "wat" -version = "1.200.0" +version = "1.225.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "776cbd10e217f83869beaa3f40e312bb9e91d5eee29bbf6f560db1261b6a4c3d" +checksum = "89e72a33942234fd0794bcdac30e43b448de3187512414267678e511c6755f11" dependencies = [ - "wast 200.0.0", + "wast 225.0.0", ] [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", @@ -758,11 +986,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "winapi", + "windows-sys", ] [[package]] @@ -773,22 +1001,23 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" -version = "0.52.0" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d380ba1dc7187569a8a9e91ed34b8ccfc33123bbacb8c0aed2d1ad7f3ef2dc5f" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", + "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", @@ -797,42 +1026,48 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68e5dcfb9413f53afd9c8f86e56a7b4d86d9a2fa26090ea2dc9e40fba56c6ec6" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dab469ebbc45798319e69eebf92308e541ce46760b49b18c6b3fe5e8965b30f" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" -version = "0.52.3" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4e9b6a7cac734a8b4138a4e1044eac3404d8326b6c0f939276560687a033fb" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b0ec9c422ca95ff34a78755cfa6ad4a51371da2a5ace67500cf7ca5f232c58" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704131571ba93e89d7cd43482277d6632589b18ecf4468f591fbae0a8b101614" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42079295511643151e98d61c38c0acc444e52dd42ab456f7ccfd5152e8ecf21c" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" -version = "0.52.3" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml index e0103d720c..f038c90e59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,8 @@ wat = "1.0.83" wast = "212.0.0" criterion = { version = "0.5.1", features = ["html_reports"] } hexf = "0.2.1" +serde = { version = "1.0.217", features = ["derive"] } +serde_json = "1.0.138" [features] default = ["hooks"] diff --git a/ci-tools/compare_testsuite.py b/ci-tools/compare_testsuite.py new file mode 100644 index 0000000000..e489309717 --- /dev/null +++ b/ci-tools/compare_testsuite.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 + +import json +import sys +import os.path + + +def sanitize_filepath(path: str) -> str: + if path.startswith("./tests/specification/testsuite/"): + path = path[len("./tests/specification/testsuite/") :] + return path + + +def sanatize_table_item(item: str) -> str: + if item is None: + return "-" + new = "" + new = item.replace("`", "'") + new = new.replace("|", "/") + new = new.replace("\n", " ") + + return new + + +def print_missing(original: bool, new: bool): + if not original and new: + print( + "Uh-oh! It looks like the original testsuite results do not exist! " + "This is likely due to the testsuite not being updated on the target branch. " + "This PR will fix this issue. No comparison possible. " + ) + elif original and not new: + print( + "Uh-oh! It looks like this PR couldn't generate the testsuite results. " + "Please make sure this isn't indicative of a bigger error! " + ) + elif not original and not new: + print( + "Uh-oh! It looks like the original testsuite results and the new results are both missing! " + "This is certainly indicative of an error in either the branches or this GH action! " + ) + + +def get_summary(entries) -> str: + """ + Logic: + - For each entry + - If script error, print error + - If Assert: + - Count number of passing and failing asserts, calculate percentage + """ + result = "" + result += "
Click here to open\n\n" + result += "| **File** | **Passed Asserts** | **Failed Asserts** | **% Passed** | **Notes** |\n" + result += "|:--------:|:------------------:|:------------------:|:------------:|-----------|\n" + + for entry in entries: + file = sanitize_filepath(entry["filepath"]) + result += f"| {file}" + if "Assert" in entry["data"]: + failed = 0 + passed = 0 + for an_assert in entry["data"]["Assert"]["results"]: + if an_assert["error"] is not None: + failed += 1 + else: + passed += 1 + + total = failed + passed + if total != 0: + percent = round(passed / total * 100, 2) + else: + percent = 0 + result += f"| {passed} / {total} | {failed} / {total} | {percent}% | - |\n" + else: # "ScriptError" + script_error = entry["data"]["ScriptError"] + result += f"| - | - | - |" + # result += f'Error: `{sanatize_table_item(script_error["error"])}`
' + result += f'Context: `{sanatize_table_item(script_error["context"])}`
' + result += f'Line: `{script_error["line_number"]}`
' + # result += f'Command: `{sanatize_table_item(script_error["command"])}`' + result += "|\n" + + result += "\n\n
\n" + return result + + +def get_delta(old_entries, new_entries) -> str: + """ + Delta cases: + 1. File deleted + - in old entries (?) + - in new entries (?) + 2. ScriptError + - in old but not in new (good) + - in new but not in old (bad) + 3. Asserts + - modified file/asserts (?) + - Error status: + i. Failing in old, Passing in new (good) + ii. Failing in new, Passing in old (bad) + iii. Some failing and some passing in new (good and bad = ?) + - (Basically a combination of case [i] and case [ii]) + """ + result = "" + header = "" + header += "| **File** | **Notes** | ❓ |\n" + header += "|:--------:|:---------:|:--:|\n" + + def find_entry(haystack, filepath): + for entry in haystack: + if entry["filepath"] == filepath: + return entry + return None + + def find_assert(haystack, line, command): + for an_assert in haystack: + if an_assert["line_number"] == line and an_assert["command"] == command: + return an_assert + return None + + # Get reunion of entries + all_entries = list(old_entries) + for entry in new_entries: + if find_entry(old_entries, entry["filepath"]) is None: + all_entries.append(entry) + + for entry in all_entries: + full_file = entry["filepath"] + file = sanitize_filepath(entry["filepath"]) + + old_entry = find_entry(old_entries, full_file) + new_entry = find_entry(new_entries, full_file) + + if old_entry is not None and new_entry is not None: + # First, compare if script error + se_old = "ScriptError" in old_entry["data"] + se_new = "ScriptError" in new_entry["data"] + + if se_old and not se_new: + result += f"| {file} | File now compiles | ✅ |\n" + elif not se_old and se_new: + result += f"| {file} | File no longer compiles | ❌ |\n" + elif not se_old and not se_new: + # Secondly, test if file is the same. + asserts_old = old_entry["data"]["Assert"]["results"] + asserts_new = new_entry["data"]["Assert"]["results"] + same_file_contents = len(asserts_old) == len(asserts_new) and all( + [ + find_assert(asserts_new, old["line_number"], old["command"]) is not None + for old in asserts_old + ] + ) + + if not same_file_contents: + result += f"| {file} | File has changed. Cannot check | ⚠️ |\n" + else: + # Sort by line number + asserts_old = sorted( + asserts_old, key=lambda an_assert: an_assert["line_number"] + ) + asserts_new = sorted( + asserts_new, key=lambda an_assert: an_assert["line_number"] + ) + + new_passing = 0 + new_failing = 0 + for i in range(len(asserts_old)): + old_is_err = asserts_old[i]["error"] is not None + new_is_err = asserts_new[i]["error"] is not None + + if old_is_err and not new_is_err: + new_passing += 1 + elif not old_is_err and new_is_err: + new_failing += 1 + + if new_passing != 0 or new_failing != 0: + result += f"| {file} | " + if new_passing != 0 and new_failing == 0: + result += f"+{new_passing} asserts PASS | ✅ |\n" + elif new_passing == 0 and new_failing != 0: + result += f"-{new_failing} asserts FAIL | ❌ |\n" + else: + result += f"+{new_passing} asserts PASS
-{new_failing} asserts FAIL | ⚠️ |\n" + + else: # Script error both in old and new + pass + elif new_entry is None: + result += f"| {file} | File missing in this PR | ⚠️ |\n" + elif old_entry is None: + result += f"| {file} | File missing in target branch | ⚠️ |\n" + + if result != "": + return header + result + else: + return " No changes detected. " + + +def main(): + if len(sys.argv) != 3: + print("Usage: script.py ") + sys.exit(1) + + original_path = sys.argv[1] + new_path = sys.argv[2] + + print("# 🗒️ Testsuite Report\n\n") + + original_exists = os.path.isfile(original_path) + new_exists = os.path.isfile(new_path) + if not original_exists or not new_exists: + print_missing(original_exists, new_exists) + return + + old_data = None + with open(original_path, "r") as f: + old_data = json.load(f) + + new_data = None + with open(new_path, "r") as f: + new_data = json.load(f) + + if original_exists and new_exists: + print("## PR delta: \n") + print(get_delta(old_data["entries"], new_data["entries"])) + print("\n") + + # print("## Target summary: \n") + # print(get_overview(old_data["entries"])) + # print("\n") + + if new_exists: + print("## PR summary: \n") + print(get_summary(new_data["entries"])) + print("\n") + + +if __name__ == "__main__": + main() diff --git a/flake.nix b/flake.nix index ab5e230e16..f000bf5279 100644 --- a/flake.nix +++ b/flake.nix @@ -132,6 +132,7 @@ nodePackages.prettier treefmtEval.config.build.wrapper typst # for the whitepaper + python3 # for comparing official testsuite results ]; env = [ { @@ -187,6 +188,26 @@ ''; help = "start typst watch loop for the whitepaper"; } + { + name = "generate-testsuite-report"; + # TODO maybe accept the name of the target branch as an argument and use it instead of `main` + command = '' + ( + cd $PRJ_ROOT + TESTSUITE_SAVE=1 cargo test -- spec_tests --show-output + cp testsuite_results.json new.json + mkdir .main_clone + git clone --depth 1 --single-branch --no-tags --recursive -b main $(git config --get remote.origin.url) .main_clone + cd .main_clone + TESTSUITE_SAVE=1 cargo test -- spec_tests --show-output + mv testsuite_results.json ../old.json + cd .. + rm -rf .main_clone + python3 ./ci-tools/compare_testsuite.py old.json new.json > testsuite_report.md + ) + ''; + help = "generates a comparison document for the official wasm testsuite w.r.t. project main branch"; + } ]; } ); diff --git a/tests/specification/ci_reports.rs b/tests/specification/ci_reports.rs new file mode 100644 index 0000000000..3349f49155 --- /dev/null +++ b/tests/specification/ci_reports.rs @@ -0,0 +1,84 @@ +use super::reports::{WastError, WastSuccess, WastTestReport}; + +#[derive(serde::Serialize)] +pub struct CIFullReport { + pub entries: Vec, +} + +impl CIFullReport { + pub fn new(report: &[WastTestReport]) -> Self { + Self { + entries: report.iter().map(CIReportHeader::new).collect(), + } + } +} + +#[derive(serde::Serialize)] +pub struct CIReportHeader { + pub filepath: String, + pub data: CIReportData, +} +impl CIReportHeader { + fn new(report: &WastTestReport) -> Self { + let filepath = match report { + WastTestReport::Asserts(assert_report) => assert_report.filename.clone(), + WastTestReport::ScriptError(script_error) => script_error.filename.clone(), + }; + + Self { + filepath, + data: CIReportData::new(report), + } + } +} + +#[derive(serde::Serialize)] +pub enum CIReportData { + Assert { + results: Vec, + }, + ScriptError { + error: String, + context: String, + line_number: Option, + command: Option, + }, +} +impl CIReportData { + fn new(report: &WastTestReport) -> Self { + match report { + WastTestReport::Asserts(assert_report) => Self::Assert { + results: assert_report.results.iter().map(CIAssert::new).collect(), + }, + WastTestReport::ScriptError(script_error) => Self::ScriptError { + error: script_error.error.to_string(), + context: script_error.context.clone(), + line_number: script_error.line_number, + command: script_error.command.clone(), + }, + } + } +} + +#[derive(serde::Serialize)] +pub struct CIAssert { + pub error: Option, + pub line_number: u32, + pub command: String, +} +impl CIAssert { + fn new(res: &Result) -> Self { + match res { + Ok(success) => Self { + error: None, + line_number: success.line_number, + command: success.command.clone(), + }, + Err(err) => Self { + error: Some(err.inner.to_string()), + line_number: err.line_number, + command: err.command.clone(), + }, + } + } +} diff --git a/tests/specification/files.rs b/tests/specification/files.rs new file mode 100644 index 0000000000..c99c32ee67 --- /dev/null +++ b/tests/specification/files.rs @@ -0,0 +1,64 @@ +use std::ffi::{OsStr, OsString}; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone)] +pub struct Filter { + pub mode: FilterMode, + pub files: Vec, +} + +#[allow(dead_code)] // Currently, we can always use only one variant at a time +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum FilterMode { + Include, + Exclude, +} + +impl Filter { + fn allows(&self, file_path: &Path) -> bool { + // If the file does not have a file name, we simply disallow it + let Some(file_name) = file_path.file_name() else { + return false; + }; + + // Assume filter includes for now + let result = self + .files + .iter() + .any(|os_string| os_string.as_os_str() == file_name); + + // Now if the filter excludes, invert the result + result ^ (self.mode == FilterMode::Exclude) + } +} + +/// Simple depth-first directory traversal with a filter for `wast` files +pub fn find_wast_files(base_path: &Path, filter: &Filter) -> std::io::Result> { + let mut paths = vec![]; + + // The stack containing all directories we still have to traverse. At first contains only the base directory. + let mut read_dirs = vec![std::fs::read_dir(base_path)?]; + + while let Some(last_read_dir) = read_dirs.last_mut() { + let Some(entry) = last_read_dir.next() else { + read_dirs.pop(); + continue; + }; + + let entry = entry?; + let meta = entry.metadata()?; + let path = entry.path(); + + if filter.allows(&path) { + if meta.is_file() && entry.path().extension() == Some(OsStr::new("wast")) { + paths.push(entry.path()) + } + + if meta.is_dir() { + read_dirs.push(std::fs::read_dir(path)?); + } + } + } + + Ok(paths) +} diff --git a/tests/specification/mod.rs b/tests/specification/mod.rs index dc80cce6bd..e8fcbd922f 100644 --- a/tests/specification/mod.rs +++ b/tests/specification/mod.rs @@ -1,129 +1,94 @@ -use std::{ - fs, - path::{Path, PathBuf}, -}; - +use ci_reports::CIFullReport; +use files::{Filter, FilterMode}; use reports::WastTestReport; +use std::ffi::OsString; +use std::fmt::Write as _; +use std::path::Path; +mod ci_reports; +mod files; mod reports; mod run; mod test_errors; -enum Filter { - #[allow(dead_code)] - Include(FnF), - #[allow(dead_code)] - Exclude(FnF), -} - -struct Report { - #[allow(dead_code)] - fp: PathBuf, - report: WastTestReport, -} - -struct FnF { - #[allow(dead_code)] - files: Option>, - #[allow(dead_code)] - folders: Option>, -} - -impl Default for FnF { - fn default() -> Self { - Self { - files: None, - folders: None, - } - } -} - #[test_log::test] pub fn spec_tests() { - // so we don't see unnecessary stacktraces of catch_unwind (sadly this also means we don't see panics from outside catch_unwind either) - std::panic::set_hook(Box::new(|_| {})); + let filter = Filter { + mode: FilterMode::Exclude, + files: vec![OsString::from("proposals")], + }; - let filters = Filter::Exclude(FnF { - folders: Some(vec!["proposals".to_string()]), - ..Default::default() - }); - - let paths = get_wast_files(Path::new("./tests/specification/testsuite/"), &filters) + let paths = files::find_wast_files(Path::new("./tests/specification/testsuite/"), &filter) .expect("Failed to find testsuite"); - // let pb: PathBuf = "./tests/specification/testsuite/table_get.wast".into(); - // let mut paths = Vec::new(); - // paths.push(pb); - - assert!(paths.len() > 0, "Submodules not instantiated"); + assert!(!paths.is_empty(), "Submodules not instantiated"); - let mut successful_reports = 0; - let mut failed_reports = 0; - let mut compile_error_reports = 0; - let mut reports: Vec = Vec::with_capacity(paths.len()); + // Some statistics about the reports + let mut num_failures = 0; + let mut num_script_errors = 0; - let mut longest_string_len: usize = 0; + // Used for padding of filenames with spaces later + let mut longest_filename_len: usize = 0; - for test_path in paths { - let mut report = run::run_spec_test(test_path.to_str().unwrap()); + let reports: Vec = paths + .into_iter() + .map(|path| run::run_spec_test(path.to_str().unwrap())) + .inspect(|report| { + match report { + reports::WastTestReport::Asserts(assert_report) => { + longest_filename_len = longest_filename_len.max(assert_report.filename.len()); - match &mut report { - reports::WastTestReport::Asserts(ref mut assert_report) => { - // compute auxiliary data - assert_report.compute_data(); - if assert_report.filename.len() > longest_string_len { - longest_string_len = assert_report.filename.len(); + if assert_report.has_errors() { + num_failures += 1; + } } - if assert_report.has_errors() { - failed_reports += 1; - } else { - successful_reports += 1; + reports::WastTestReport::ScriptError(_) => { + num_script_errors += 1; } - } - reports::WastTestReport::CompilationError(_) => { - compile_error_reports += 1; - } - }; - - let rep = Report { - fp: test_path.clone(), - report, - }; + }; + }) + .collect(); - reports.push(rep); - } + // Calculate another required statistic + let num_successes = reports.len() - num_script_errors - num_failures; - let mut no_compile_errors_reports = reports + // Collect all reports without errors along with some statistic + let mut successful_mini_tests = 0; + let mut total_mini_tests = 0; + let mut assert_reports: Vec<&reports::AssertReport> = reports .iter() - .filter_map(|e| match &e.report { + .filter_map(|r| match r { WastTestReport::Asserts(asserts) => Some(asserts), - _ => None, + WastTestReport::ScriptError(_) => None, + }) + .inspect(|assert_report| { + successful_mini_tests += assert_report.passed_asserts(); + total_mini_tests += assert_report.total_asserts(); }) - .collect::>(); - no_compile_errors_reports.sort_by(|a, b| b.percentage.total_cmp(&a.percentage)); + .collect(); - let mut successful_mini_tests = 0; - let mut total_mini_tests = 0; + // Sort all reports without errors for displaying it to the user later + assert_reports.sort_by(|a, b| { + b.percentage_asserts_passed() + .total_cmp(&a.percentage_asserts_passed()) + }); let mut final_status: String = String::new(); // Printing success rate per file for those that did NOT error out when compiling - for report in no_compile_errors_reports { - final_status += format!( - "Report for {:filename_width$}: Tests: {:passed_width$} Passed, {:failed_width$} Failed --- {:percentage_width$.2}%\n", + for report in assert_reports { + writeln!(final_status, + "Report for {:filename_width$}: Tests: {:passed_width$} Passed, {:failed_width$} Failed --- {:percentage_width$.2}%", report.filename, - report.successful, - report.failed, - report.percentage, - filename_width = longest_string_len + 1, + report.passed_asserts(), + report.failed_asserts(), + report.percentage_asserts_passed(), + filename_width = longest_filename_len + 1, passed_width = 7, failed_width = 7, percentage_width = 6 - ).as_str(); + ).expect("writing into strings to never fail"); - successful_mini_tests += report.successful; - total_mini_tests += report.total; - - if report.successful < report.total { + if report.passed_asserts() < report.total_asserts() { println!("{}", report); } } @@ -136,7 +101,7 @@ pub fn spec_tests() { successful_mini_tests, total_mini_tests - successful_mini_tests, if total_mini_tests == 0 { 0.0 } else {(successful_mini_tests as f64) * 100.0 / (total_mini_tests as f64)}, - filename_width = longest_string_len + 1, + filename_width = longest_filename_len + 1, passed_width = 7, failed_width = 7, percentage_width = 6 @@ -144,119 +109,14 @@ pub fn spec_tests() { println!( "Tests: {} Passed, {} Failed, {} Compilation Errors", - successful_reports, failed_reports, compile_error_reports + num_successes, num_failures, num_script_errors ); -} - -#[allow(dead_code)] -// See: https://stackoverflow.com/a/76820878 -fn get_wast_files( - base_path: &Path, - // run_only_these_tests: &Vec, - filters: &Filter, -) -> Result, std::io::Error> { - let mut buf = vec![]; - let entries = fs::read_dir(base_path)?; - for entry in entries { - let entry = entry?; - let meta = entry.metadata()?; + // Optional: We need to save the result to a file for CI Regression Analysis + if std::option_env!("TESTSUITE_SAVE").is_some() { + let ci_report = CIFullReport::new(&reports); + let output_file = std::fs::File::create("./testsuite_results.json").unwrap(); - if meta.is_dir() { - if should_add_folder_to_buffer(&entry.path(), &filters) { - let mut subdir = get_wast_files(&entry.path(), &filters)?; - buf.append(&mut subdir); - } - } - - if meta.is_file() && entry.path().extension().unwrap_or_default() == "wast" { - if should_add_file_to_buffer(&entry.path(), &filters) { - buf.push(entry.path()) - } - } - } - - Ok(buf) -} - -fn should_add_file_to_buffer(file_path: &PathBuf, filters: &Filter) -> bool { - match filters { - Filter::Exclude(ref fnf) => match &fnf.files { - None => true, - Some(files) => { - if files.is_empty() { - return true; - } - - if let Some(file_name) = file_path.file_name() { - if files.contains(&file_name.to_str().unwrap().to_owned()) { - false - } else { - true - } - } else { - false - } - } - }, - Filter::Include(ref fnf) => match &fnf.files { - None => false, - Some(files) => { - if files.is_empty() { - return false; - } - - if let Some(file_name) = file_path.file_name() { - if files.contains(&file_name.to_str().unwrap().to_owned()) { - true - } else { - false - } - } else { - false - } - } - }, - } -} - -fn should_add_folder_to_buffer(file_path: &PathBuf, filters: &Filter) -> bool { - match filters { - Filter::Exclude(ref fnf) => match &fnf.folders { - None => true, - Some(folders) => { - if folders.is_empty() { - return true; - } - - if let Some(file_name) = file_path.file_name() { - if folders.contains(&file_name.to_str().unwrap().to_owned()) { - false - } else { - true - } - } else { - false - } - } - }, - Filter::Include(ref fnf) => match &fnf.folders { - None => false, - Some(folders) => { - if folders.is_empty() { - return false; - } - - if let Some(file_name) = file_path.file_name() { - if folders.contains(&file_name.to_str().unwrap().to_owned()) { - true - } else { - false - } - } else { - false - } - } - }, + serde_json::to_writer_pretty(output_file, &ci_report).unwrap(); } } diff --git a/tests/specification/reports.rs b/tests/specification/reports.rs index e32baf1639..d6e7527ead 100644 --- a/tests/specification/reports.rs +++ b/tests/specification/reports.rs @@ -1,8 +1,8 @@ use std::error::Error; pub struct WastSuccess { - line_number: u32, - command: String, + pub line_number: u32, + pub command: String, } impl WastSuccess { @@ -15,39 +15,69 @@ impl WastSuccess { } pub struct WastError { - inner: Box, - line_number: Option, - command: String, + pub inner: Box, + pub line_number: u32, + pub command: String, } impl WastError { pub fn new(error: Box, line_number: u32, command: &str) -> Self { Self { inner: error, - line_number: Some(line_number), + line_number, command: command.to_string(), } } } +/// Wast script executed successfuly. The results of asserts (pass/fail) are +/// stored here. pub struct AssertReport { pub filename: String, pub results: Vec>, - pub successful: usize, - pub failed: usize, - pub total: usize, - pub percentage: f64, } -impl Default for AssertReport { - fn default() -> Self { +impl AssertReport { + pub fn new(filename: &str) -> Self { Self { - filename: String::from(""), + filename: filename.to_string(), results: Vec::new(), - successful: 0, - failed: 0, - total: 0, - percentage: 0.0, + } + } + + pub fn push_success(&mut self, success: WastSuccess) { + self.results.push(Ok(success)); + } + + pub fn push_error(&mut self, error: WastError) { + self.results.push(Err(error)); + } + + pub fn compile_report(self) -> WastTestReport { + WastTestReport::Asserts(self) + } + + pub fn has_errors(&self) -> bool { + self.results.iter().any(|r| r.is_err()) + } + + pub fn total_asserts(&self) -> u32 { + self.results.len() as u32 + } + + pub fn passed_asserts(&self) -> u32 { + self.results.iter().filter(|el| el.is_ok()).count() as u32 + } + + pub fn failed_asserts(&self) -> u32 { + self.total_asserts() - self.passed_asserts() + } + + pub fn percentage_asserts_passed(&self) -> f32 { + if self.total_asserts() == 0 { + 0.0 + } else { + (self.passed_asserts() as f32) * 100.0 / (self.total_asserts() as f32) } } } @@ -73,17 +103,7 @@ impl std::fmt::Display for AssertReport { writeln!( f, "❌ {}:{} -> {}", - self.filename, - match error.line_number { - None => u32::MAX.to_string(), - Some(line_number) => - if line_number == u32::MAX { - "?".to_string() - } else { - line_number.to_string() - }, - }, - error.command + self.filename, error.line_number, error.command )?; writeln!(f, " Error: {}", error.inner)?; } @@ -94,82 +114,69 @@ impl std::fmt::Display for AssertReport { } } -impl AssertReport { - pub fn new(filename: &str) -> Self { +/// An error originating from within the WAST Script. If a non-assert directive +/// fails, a ScriptError will be raised. +pub struct ScriptError { + pub filename: String, + pub error: Box, + pub context: String, + #[allow(unused)] + pub line_number: Option, + #[allow(unused)] + pub command: Option, +} + +impl ScriptError { + pub fn new( + filename: &str, + error: Box, + context: &str, + line_number: u32, + command: &str, + ) -> Self { Self { filename: filename.to_string(), - results: Vec::new(), - ..Default::default() + error, + context: context.to_string(), + line_number: Some(line_number), + command: Some(command.to_string()), } } - pub fn compute_data(&mut self) { - self.total = self.results.len(); - self.successful = self.results.iter().filter(|el| el.is_ok()).count(); - self.failed = self.total - self.successful; - self.percentage = if self.total == 0 { - 0.0 - } else { - (self.successful as f64) * 100.0 / (self.total as f64) - }; - } - - pub fn push_success(&mut self, success: WastSuccess) { - self.results.push(Ok(success)); - } - - pub fn push_error(&mut self, error: WastError) { - self.results.push(Err(error)); - } - - pub fn compile_report(self) -> WastTestReport { - return WastTestReport::Asserts(self); - } - - pub fn has_errors(&self) -> bool { - self.results.iter().any(|r| r.is_err()) - } -} - -pub struct CompilationError { - inner: Box, - filename: String, - context: String, -} - -impl CompilationError { - pub fn new(error: Box, filename: &str, context: &str) -> Self { + pub fn new_lineless(filename: &str, error: Box, context: &str) -> Self { Self { - inner: error, filename: filename.to_string(), + error, context: context.to_string(), + line_number: None, + command: None, } } pub fn compile_report(self) -> WastTestReport { - return WastTestReport::CompilationError(self); + WastTestReport::ScriptError(self) } } pub enum WastTestReport { + /// The script ran successfully, having directives run successfuly (though + /// not necessarily meaning all asserts pass!) Asserts(AssertReport), - CompilationError(CompilationError), + /// The script could not run successfully, a non-assert directive failed in + /// such a way the script cannot continue running. + ScriptError(ScriptError), } -// .------------------------. -// | Display Implementation | -// '------------------------' - impl std::fmt::Display for WastTestReport { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - WastTestReport::CompilationError(error) => { + WastTestReport::ScriptError(error) => { writeln!(f, "------ {} ------", error.filename)?; writeln!(f, "⚠ Compilation Failed ⚠")?; writeln!(f, "Context: {}", error.context)?; - writeln!(f, "Error: {}", error.inner)?; + writeln!(f, "Error: {}", error.error)?; writeln!(f, "~~~~~~~~~~~~~~~~")?; - writeln!(f, "")?; + writeln!(f)?; } WastTestReport::Asserts(assert_report) => { writeln!(f, "------ {} ------", assert_report.filename)?; @@ -178,14 +185,14 @@ impl std::fmt::Display for WastTestReport { let failed_asserts = assert_report.results.iter().filter(|r| r.is_err()).count(); let total_asserts = assert_report.results.len(); - writeln!(f, "")?; + writeln!(f)?; writeln!( f, "Execution finished. Passed: {}, Failed: {}, Total: {}", passed_asserts, failed_asserts, total_asserts )?; writeln!(f, "~~~~~~~~~~~~~~~~")?; - writeln!(f, "")?; + writeln!(f)?; } } diff --git a/tests/specification/run.rs b/tests/specification/run.rs index c9f8a6bdd8..1a5a521583 100644 --- a/tests/specification/run.rs +++ b/tests/specification/run.rs @@ -1,22 +1,7 @@ -/* -# This file incorporates code from Wasmtime, originally -# available at https://github.com/bytecodealliance/wasmtime. -# -# The original code is licensed under the Apache License, Version 2.0 -# (the "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -*/ +use std::any::Any; use std::error::Error; -use std::panic::catch_unwind; use std::panic::AssertUnwindSafe; +use std::panic::UnwindSafe; use wasm::function_ref::FunctionRef; use wasm::RuntimeError; @@ -25,32 +10,37 @@ use wasm::DEFAULT_MODULE; use wasm::{validate, RuntimeInstance}; use wast::core::WastArgCore; use wast::core::WastRetCore; +use wast::QuoteWat; use wast::WastArg; use crate::specification::reports::*; use crate::specification::test_errors::*; -pub fn to_wasm_testsuite_string(runtime_error: RuntimeError) -> std::string::String { +pub fn to_wasm_testsuite_string(runtime_error: RuntimeError) -> Result> { + let not_represented = Err(GenericError::new_boxed( + "Runtime error not represented in WAST", + )); + match runtime_error { - RuntimeError::DivideBy0 => "integer divide by zero", - RuntimeError::UnrepresentableResult => "integer overflow", - RuntimeError::FunctionNotFound => todo!(), - RuntimeError::StackSmash => todo!(), - RuntimeError::BadConversionToInteger => "invalid conversion to integer", - - RuntimeError::MemoryAccessOutOfBounds => "out of bounds memory access", - RuntimeError::TableAccessOutOfBounds => "out of bounds table access", - RuntimeError::ElementAccessOutOfBounds => todo!(), - - RuntimeError::UninitializedElement => "uninitialized element", - RuntimeError::SignatureMismatch => "indirect call type mismatch", - RuntimeError::ExpectedAValueOnTheStack => todo!(), - - RuntimeError::UndefinedTableIndex => "undefined element", - RuntimeError::ModuleNotFound => "module not found", - RuntimeError::UnmetImport => "unmet import", + RuntimeError::DivideBy0 => Ok("integer divide by zero"), + RuntimeError::UnrepresentableResult => Ok("integer overflow"), + RuntimeError::FunctionNotFound => not_represented, + RuntimeError::StackSmash => not_represented, + RuntimeError::BadConversionToInteger => Ok("invalid conversion to integer"), + + RuntimeError::MemoryAccessOutOfBounds => Ok("out of bounds memory access"), + RuntimeError::TableAccessOutOfBounds => Ok("out of bounds table access"), + RuntimeError::ElementAccessOutOfBounds => not_represented, + + RuntimeError::UninitializedElement => Ok("uninitialized element"), + RuntimeError::SignatureMismatch => Ok("indirect call type mismatch"), + RuntimeError::ExpectedAValueOnTheStack => not_represented, + + RuntimeError::UndefinedTableIndex => Ok("undefined element"), + RuntimeError::ModuleNotFound => Ok("module not found"), + RuntimeError::UnmetImport => Ok("unmet import"), } - .to_string() + .map(|s| s.to_string()) } /// Attempt to unwrap the result of an expression. If the expression is an `Err`, then `return` the @@ -67,96 +57,56 @@ macro_rules! try_to { }; } -#[derive(Debug)] -enum ErrEVI { - Encode, - Validate, - Instantiate, -} +/// Clear the bytes and runtime instance before calling this function +fn encode(module: &mut wast::QuoteWat) -> Result, Box> { + match &module { + QuoteWat::QuoteComponent(..) | QuoteWat::Wat(wast::Wat::Component(..)) => { + return Err(GenericError::new_boxed( + "Component modules are not supported", + )) + } + QuoteWat::Wat(..) | QuoteWat::QuoteModule(..) => (), + }; -impl std::fmt::Display for ErrEVI { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self) - } + let inner_bytes = module.encode().map_err(Box::new)?; + Ok(inner_bytes) } -impl std::error::Error for ErrEVI {} +fn validate_instantiate(bytes: &[u8]) -> Result, Box> { + let validation_info = catch_unwind_and_suppress_panic_handler(|| validate(bytes)) + .map_err(PanicError::from_panic_boxed)? + .map_err(WasmInterpreterError::new_boxed)?; -/// Clear the bytes and runtime instance before calling this function -fn encode_validate_instantiate<'a>( - module: &mut wast::QuoteWat, - bytes: &'a mut Option>, - runtime_inst: &'a mut Option>, -) -> Result<(), ErrEVI> { - use wast::*; - let is_module = match &module { - QuoteWat::QuoteComponent(..) | QuoteWat::Wat(Wat::Component(..)) => false, - QuoteWat::Wat(..) | QuoteWat::QuoteModule(..) => true, - }; + let runtime_instance = + catch_unwind_and_suppress_panic_handler(|| RuntimeInstance::new(&validation_info)) + .map_err(PanicError::from_panic_boxed)? + .map_err(WasmInterpreterError::new_boxed)?; - if is_module { - let inner_bytes = module.encode(); - - match inner_bytes { - Err(_) => Err(ErrEVI::Encode), - Ok(inner_bytes) => { - bytes.replace(inner_bytes); - let validation_info_attempt = catch_unwind(|| validate(bytes.as_ref().unwrap())); - - match validation_info_attempt { - Err(_) => Err(ErrEVI::Validate), - Ok(validation_info) => match validation_info { - Err(_) => Err(ErrEVI::Validate), - Ok(validation_info) => { - let runtime_instance_result = - catch_unwind(|| RuntimeInstance::new(&validation_info)); - - match runtime_instance_result { - Err(_) => Err(ErrEVI::Instantiate), - Ok(runtime_instance) => match runtime_instance { - Err(_) => Err(ErrEVI::Instantiate), - Ok(runtime_instance) => { - runtime_inst.replace(runtime_instance); - Ok(()) - } - }, - } - } - }, - } - } - } - } else { - Err(ErrEVI::Encode) - } + Ok(runtime_instance) } pub fn run_spec_test(filepath: &str) -> WastTestReport { // -=-= Initialization =-=- - let contents = try_to!( - std::fs::read_to_string(filepath).map_err(|err| CompilationError::new( - Box::new(err), - filepath, - "failed to open wast file" - ) - .compile_report()) - ); - - let buf = + let contents = try_to!( - wast::parser::ParseBuffer::new(&contents).map_err(|err| CompilationError::new( - Box::new(err), + std::fs::read_to_string(filepath).map_err(|err| ScriptError::new_lineless( filepath, - "failed to create wast buffer" + Box::new(err), + "failed to open wast file", ) .compile_report()) ); + let buf = try_to!(wast::parser::ParseBuffer::new(&contents).map_err(|err| { + ScriptError::new_lineless(filepath, Box::new(err), "failed to create wast buffer") + .compile_report() + })); + let wast = try_to!( - wast::parser::parse::(&buf).map_err(|err| CompilationError::new( - Box::new(err), + wast::parser::parse::(&buf).map_err(|err| ScriptError::new_lineless( filepath, + Box::new(err), "failed to parse wast file" ) .compile_report()) @@ -165,126 +115,71 @@ pub fn run_spec_test(filepath: &str) -> WastTestReport { // -=-= Testing & Compilation =-=- let mut asserts = AssertReport::new(filepath); - // We need to keep the wasm_bytes in-scope for the lifetime of the interpeter. + // We need to keep the wasm_bytes in-scope for the lifetime of the interpreter. // As such, we hoist the bytes into an Option, and assign it once a module directive is found. #[allow(unused_assignments)] let mut wasm_bytes: Option> = None; - let mut interpeter = None; + let mut interpreter = None; for directive in wast.directives { match directive { wast::WastDirective::Wat(mut quoted) => { // If we fail to compile or to validate the main module, then we should treat this // as a fatal (compilation) error. - let encoded = try_to!(quoted.encode().map_err(|err| CompilationError::new( - Box::new(err), - filepath, - "failed to encode main module's wat" - ) - .compile_report())); - - wasm_bytes = Some(encoded); + wasm_bytes = Some(try_to!(encode(&mut quoted).map_err(|err| { + ScriptError::new( + filepath, + err, + "Module directive (WAT) failed in encoding step.", + get_linenum(&contents, quoted.span()), + get_command(&contents, quoted.span()), + ) + .compile_report() + }))); - let validation_attempt = catch_unwind(|| { - validate(wasm_bytes.as_ref().unwrap()).map_err(|err| { - CompilationError::new( - Box::new(WasmInterpreterError(err)), + interpreter = Some(try_to!(validate_instantiate(wasm_bytes.as_ref().unwrap()) + .map_err(|err| { + ScriptError::new( filepath, - "main module validation failed", - ) - .compile_report() - }) - }); - - let validation_info = match validation_attempt { - Ok(original_result) => try_to!(original_result), - Err(panic) => { - // TODO: Do we want to exit on panic? State may be left in an inconsistent state, and cascading panics may occur. - let err = if let Ok(msg) = panic.downcast::<&str>() { - Box::new(PanicError::new(&msg)) - } else { - Box::new(PanicError::new("Unknown panic")) - }; - - return CompilationError::new( err, - filepath, - "main module validation panicked", - ) - .compile_report(); - } - }; - - let runtime_instance = catch_unwind(|| RuntimeInstance::new(&validation_info)); - - let instance = match runtime_instance { - Err(_) => { - return CompilationError::new( - Box::new(ErrEVI::Instantiate), - filepath, - "failed to create runtime instance", + "Module directive (WAT) failed in validation or instantiation.", + get_linenum(&contents, quoted.span()), + get_command(&contents, quoted.span()), ) - .compile_report(); - } - Ok(instance_result) => match instance_result { - Err(e) => { - return CompilationError::new( - Box::new(WasmInterpreterError(e)), - filepath, - "failed to create runtime instance", - ) - .compile_report() - } - Ok(instance) => instance, - }, - }; - - interpeter = Some(instance); + .compile_report() + }))); } wast::WastDirective::AssertReturn { span, exec, results, } => { - if interpeter.is_none() { - return CompilationError::new( - Box::new(GenericError::new( - "Attempted to assert before module directive", - )), + if interpreter.is_none() { + return ScriptError::new( filepath, - "no module directive found", + GenericError::new_boxed("Attempted to assert before module directive"), + "Assert Return", + get_linenum(&contents, span), + get_command(&contents, span), ) .compile_report(); } - let interpeter = interpeter.as_mut().unwrap(); - - let err_or_panic: Result<(), Box> = - match catch_unwind(AssertUnwindSafe(|| { - execute_assert_return(interpeter, exec, results) - })) { - Ok(original_result) => original_result, - Err(inner) => { - // TODO: Do we want to exit on panic? State may be left in an inconsistent state, and cascading panics may occur. - if let Ok(msg) = inner.downcast::<&str>() { - Err(Box::new(PanicError::new(&msg))) - } else { - Err(Box::new(PanicError::new("Unknown panic"))) - } - } - }; + let interpreter = interpreter.as_mut().unwrap(); + + let err_or_panic = execute_assert_return(interpreter, exec, results); match err_or_panic { - Ok(_) => { + Ok(()) => { asserts.push_success(WastSuccess::new( - span.linecol_in(&contents).0 as u32 + 1, + get_linenum(&contents, span), get_command(&contents, span), )); } Err(inner) => { asserts.push_error(WastError::new( inner, - span.linecol_in(&contents).0 as u32 + 1, + get_linenum(&contents, span), get_command(&contents, span), )); } @@ -295,45 +190,32 @@ pub fn run_spec_test(filepath: &str) -> WastTestReport { exec, message, } => { - if interpeter.is_none() { - return CompilationError::new( - Box::new(GenericError::new( - "Attempted to assert before module directive", - )), + if interpreter.is_none() { + return ScriptError::new( filepath, - "no module directive found", + GenericError::new_boxed("Attempted to assert before module directive"), + "Assert Trap", + get_linenum(&contents, span), + get_command(&contents, span), ) .compile_report(); } - let interpeter = interpeter.as_mut().unwrap(); - - let err_or_panic: Result<(), Box> = - match catch_unwind(AssertUnwindSafe(|| { - execute_assert_trap(interpeter, exec, message) - })) { - Err(inner) => { - // TODO: Do we want to exit on panic? State may be left in an inconsistent state, and cascading panics may occur. - if let Ok(msg) = inner.downcast::<&str>() { - Err(Box::new(PanicError::new(&msg))) - } else { - Err(Box::new(PanicError::new("Unknown panic"))) - } - } - Ok(original_result) => original_result, - }; + let interpreter = interpreter.as_mut().unwrap(); + + let err_or_panic = execute_assert_trap(interpreter, exec, message); match err_or_panic { Ok(_) => { asserts.push_success(WastSuccess::new( - span.linecol_in(&contents).0 as u32 + 1, + get_linenum(&contents, span), get_command(&contents, span), )); } Err(inner) => { asserts.push_error(WastError::new( inner, - span.linecol_in(&contents).0 as u32 + 1, + get_linenum(&contents, span), get_command(&contents, span), )); } @@ -345,12 +227,16 @@ pub fn run_spec_test(filepath: &str) -> WastTestReport { mut module, message: _, } => { - let line_number = span.linecol_in(&contents).0 as u32 + 1; + let line_number = get_linenum(&contents, span); let cmd = get_command(&contents, span); + let error = GenericError::new_boxed( + "Module validated and instantiated successfully, when it shouldn't have", + ); - match encode_validate_instantiate(&mut module, &mut None, &mut None) { + match encode(&mut module).and_then(|bytes| validate_instantiate(&bytes).map(|_| ())) + { Err(_) => asserts.push_success(WastSuccess::new(line_number, cmd)), - Ok(_) => asserts.push_error(WastError::new("Module validated and instantiated successfully, when it shouldn't have been".into(), line_number, cmd)) + Ok(_) => asserts.push_error(WastError::new(error, line_number, cmd)), }; } @@ -359,20 +245,30 @@ pub fn run_spec_test(filepath: &str) -> WastTestReport { mut module, message: _, } => { - let line_number = span.linecol_in(&contents).0 as u32 + 1; + let line_number = get_linenum(&contents, span); let cmd = get_command(&contents, span); + let error = GenericError::new_boxed( + "Module validated and instantiated successfully, when it shouldn't have", + ); - match encode_validate_instantiate(&mut module, &mut None, &mut None) { + match encode(&mut module).and_then(|bytes| validate_instantiate(&bytes).map(|_| ())) + { Err(_) => asserts.push_success(WastSuccess::new(line_number, cmd)), - Ok(_) => asserts.push_error(WastError::new("Module validated and instantiated successfully, when it shouldn't have been".into(), line_number, cmd)) + Ok(_) => asserts.push_error(WastError::new(error, line_number, cmd)), }; } wast::WastDirective::Register { span, name: _, module: _, + } => { + asserts.push_error(WastError::new( + GenericError::new_boxed("Register directive not yet implemented"), + get_linenum(&contents, span), + get_command(&contents, span), + )); } - | wast::WastDirective::AssertExhaustion { + wast::WastDirective::AssertExhaustion { span, call: _, message: _, @@ -384,68 +280,95 @@ pub fn run_spec_test(filepath: &str) -> WastTestReport { } | wast::WastDirective::AssertException { span, exec: _ } => { asserts.push_error(WastError::new( - Box::new(GenericError::new("Assert directive not yet implemented")), - span.linecol_in(&contents).0 as u32 + 1, + GenericError::new_boxed("Assert directive not yet implemented"), + get_linenum(&contents, span), get_command(&contents, span), )); } wast::WastDirective::Wait { span, thread: _ } => { asserts.push_error(WastError::new( - Box::new(GenericError::new("Wait directive not yet implemented")), - span.linecol_in(&contents).0 as u32 + 1, + GenericError::new_boxed("Wait directive not yet implemented"), + get_linenum(&contents, span), get_command(&contents, span), )); } wast::WastDirective::Invoke(invoke) => { - match interpeter { - None => asserts.push_error(WastError::new( - Box::new(GenericError::new( - "Couldn't invoke, interpreter not present", - )), - u32::MAX, - "invoke", - )), - Some(ref mut interpeter) => { - let args = invoke - .args - .into_iter() - .map(arg_to_value) - .collect::>(); - - // TODO: more modules ¯\_(ツ)_/¯ - match interpeter.get_function_by_name(DEFAULT_MODULE, invoke.name) { - Err(_) => asserts.push_error(WastError::new( - Box::new(GenericError::new(&format!( - "Couldn't get the function '{}' from module '{}'", - invoke.name, "DEFAULT_MODULE" - ))), - u32::MAX, - "invoke", - )), - Ok(funcref) => { - match interpeter.invoke_dynamic_unchecked_return_ty(&funcref, args) - { - Err(e) => asserts.push_error(WastError::new( - Box::new(GenericError::new(&format!( - "failed to execute function '{}' from module '{}' - error: {:?}", - invoke.name, "DEFAULT_MODULE", e - ))), - u32::MAX, - "invoke", - )), - Ok(_) => { - asserts.push_success(WastSuccess::new(u32::MAX, "invoke")) - } - } - } - }; + if interpreter.is_none() { + return ScriptError::new( + filepath, + GenericError::new_boxed( + "Attempted to run invoke directive before interpreter instantiation.", + ), + "Invoke", + get_linenum(&contents, invoke.span), + get_command(&contents, invoke.span), + ) + .compile_report(); + } + + let interpreter = interpreter.as_mut().unwrap(); + + let args = invoke + .args + .into_iter() + .map(arg_to_value) + .collect::>(); + + let function_ref_attempt = + catch_unwind_and_suppress_panic_handler(AssertUnwindSafe(|| { + interpreter.get_function_by_name(DEFAULT_MODULE, invoke.name) + })) + .map(|result| { + result.map_err(|err| { + ScriptError::new( + filepath, + WasmInterpreterError::new_boxed(wasm::Error::RuntimeError(err)), + "Invoke directive failed to find function", + get_linenum(&contents, invoke.span), + get_command(&contents, invoke.span), + ) + .compile_report() + }) + }); + + let function_ref = match function_ref_attempt { + Ok(original_result) => try_to!(original_result), + Err(panic) => { + return ScriptError::new( + filepath, + PanicError::from_panic_boxed(panic), + "main module validation panicked", + get_linenum(&contents, invoke.span), + get_command(&contents, invoke.span), + ) + .compile_report(); } }; + + let err_or_panic: Result<_, Box> = + catch_unwind_and_suppress_panic_handler(AssertUnwindSafe(|| { + interpreter.invoke_dynamic_unchecked_return_ty(&function_ref, args) + })) + .map_err(PanicError::from_panic_boxed) + .and_then(|result| { + result.map_err(|err| { + WasmInterpreterError::new_boxed(wasm::Error::RuntimeError(err)) + }) + }); + + try_to!(err_or_panic.map_err(|inner| ScriptError::new( + filepath, + inner, + "Invoke returned error or panicked", + get_linenum(&contents, invoke.span), + get_command(&contents, invoke.span) + ) + .compile_report())); } wast::WastDirective::Thread(thread) => { asserts.push_error(WastError::new( - Box::new(GenericError::new("Thread directive not yet implemented")), - thread.span.linecol_in(&contents).0 as u32 + 1, + GenericError::new_boxed("Thread directive not yet implemented"), + get_linenum(&contents, thread.span), get_command(&contents, thread.span), )); } @@ -456,7 +379,7 @@ pub fn run_spec_test(filepath: &str) -> WastTestReport { } fn execute_assert_return( - interpeter: &mut RuntimeInstance, + interpreter: &mut RuntimeInstance, exec: wast::WastExecute, results: Vec, ) -> Result<(), Box> { @@ -468,38 +391,46 @@ fn execute_assert_return( .map(arg_to_value) .collect::>(); - let result_vals = results.into_iter().map(result_to_value).collect::>(); + let result_vals = results + .into_iter() + .map(result_to_value) + .collect::, _>>()?; let result_types = result_vals .iter() .map(|val| val.to_ty()) .collect::>(); // TODO: more modules ¯\_(ツ)_/¯ - let func = interpeter - .get_function_by_name(DEFAULT_MODULE, invoke_info.name) - .map_err(|err| Box::new(WasmInterpreterError(wasm::Error::RuntimeError(err))))?; - - let actual = interpeter - .invoke_dynamic(&func, args, &result_types) - .map_err(|err| Box::new(WasmInterpreterError(wasm::Error::RuntimeError(err))))?; + let func = catch_unwind_and_suppress_panic_handler(AssertUnwindSafe(|| { + interpreter.get_function_by_name(DEFAULT_MODULE, invoke_info.name) + })) + .map_err(PanicError::from_panic_boxed)? + .map_err(|err| WasmInterpreterError::new_boxed(wasm::Error::RuntimeError(err)))?; + + let actual = catch_unwind_and_suppress_panic_handler(AssertUnwindSafe(|| { + interpreter.invoke_dynamic(&func, args, &result_types) + })) + .map_err(PanicError::from_panic_boxed)? + .map_err(|err| WasmInterpreterError::new_boxed(wasm::Error::RuntimeError(err)))?; AssertEqError::assert_eq(actual, result_vals)?; + Ok(()) } wast::WastExecute::Get { span: _, module: _, global: _, - } => todo!("`get` directive inside `assert_return` not yet implemented"), - wast::WastExecute::Wat(_) => { - todo!("`wat` directive inside `assert_return` not yet implemented") - } + } => Err(GenericError::new_boxed( + "`get` directive inside `assert_return` not yet implemented", + )), + wast::WastExecute::Wat(_) => Err(GenericError::new_boxed( + "`wat` directive inside `assert_return` not yet implemented", + )), } - - Ok(()) } fn execute_assert_trap( - interpeter: &mut RuntimeInstance, + interpreter: &mut RuntimeInstance, exec: wast::WastExecute, message: &str, ) -> Result<(), Box> { @@ -512,24 +443,28 @@ fn execute_assert_trap( .collect::>(); // TODO: more modules ¯\_(ツ)_/¯ - let func_res = interpeter - .get_function_by_name(DEFAULT_MODULE, invoke_info.name) - .map_err(|err| Box::new(WasmInterpreterError(wasm::Error::RuntimeError(err)))); + let func_res = catch_unwind_and_suppress_panic_handler(AssertUnwindSafe(|| { + interpreter.get_function_by_name(DEFAULT_MODULE, invoke_info.name) + })) + .map_err(PanicError::from_panic_boxed)? + .map_err(|err| WasmInterpreterError::new_boxed(wasm::Error::RuntimeError(err))); - let func: FunctionRef; - match func_res { + let func: FunctionRef = match func_res { Err(e) => { return Err(e); } - Ok(func_ref) => func = func_ref, + Ok(func_ref) => func_ref, }; - let actual = interpeter.invoke_dynamic_unchecked_return_ty(&func, args); + let actual = catch_unwind_and_suppress_panic_handler(AssertUnwindSafe(|| { + interpreter.invoke_dynamic_unchecked_return_ty(&func, args) + })) + .map_err(PanicError::from_panic_boxed)?; match actual { - Ok(_) => Err(Box::new(GenericError::new("assert_trap did NOT trap"))), + Ok(_) => Err(GenericError::new_boxed("assert_trap did NOT trap")), Err(e) => { - let actual = to_wasm_testsuite_string(e); + let actual = to_wasm_testsuite_string(e)?; let expected = message; if actual.contains(expected) @@ -538,10 +473,10 @@ fn execute_assert_trap( { Ok(()) } else { - Err(Box::new(GenericError::new( + Err(GenericError::new_boxed( format!("'assert_trap': Expected '{expected}' - Actual: '{actual}'") .as_str(), - ))) + )) } } } @@ -550,10 +485,12 @@ fn execute_assert_trap( span: _, module: _, global: _, - } => todo!("`get` directive inside `assert_return` not yet implemented"), - wast::WastExecute::Wat(_) => { - todo!("`wat` directive inside `assert_return` not yet implemented") - } + } => Err(GenericError::new_boxed( + "`get` directive inside `assert_return` not yet implemented", + )), + wast::WastExecute::Wat(_) => Err(GenericError::new_boxed( + "`wat` directive inside `assert_return` not yet implemented", + )), } } @@ -590,8 +527,8 @@ pub fn arg_to_value(arg: WastArg) -> Value { } } -fn result_to_value(result: wast::WastRet) -> Value { - match result { +fn result_to_value(result: wast::WastRet) -> Result> { + let value = match result { wast::WastRet::Core(core_arg) => match core_arg { WastRetCore::I32(val) => Value::I32(val as u32), WastRetCore::I64(val) => Value::I64(val as u64), @@ -619,46 +556,43 @@ fn result_to_value(result: wast::WastRet) -> Value { Value::F64(wasm::value::F64(f64::from_bits(val.bits))) } }, - WastRetCore::V128(_) => todo!("`V128` result not yet implemented"), - WastRetCore::RefNull(rref) => match rref { - None => todo!("RefNull with no type not yet implemented"), - Some(rref) => match rref { - wast::core::HeapType::Concrete(_) => { - unreachable!("Null refs don't point to any specific reference") - } - wast::core::HeapType::Abstract { shared: _, ty } => { - use wasm::value::*; - use wast::core::AbstractHeapType::*; - match ty { - Func => Value::Ref(Ref::Func(FuncAddr::null())), - Extern => Value::Ref(Ref::Extern(ExternAddr::null())), - _ => todo!("`GC` proposal not yet implemented"), - } + WastRetCore::RefNull(Some(rref)) => match rref { + wast::core::HeapType::Concrete(_) => { + unreachable!("Null refs don't point to any specific reference") + } + wast::core::HeapType::Abstract { shared: _, ty } => { + use wasm::value::*; + use wast::core::AbstractHeapType::*; + match ty { + Func => Value::Ref(Ref::Func(FuncAddr::null())), + Extern => Value::Ref(Ref::Extern(ExternAddr::null())), + _ => todo!("`GC` proposal not yet implemented"), } - }, + } }, - WastRetCore::RefExtern(_) => { - todo!("`RefExtern` result not yet implemented") - } - WastRetCore::RefHost(_) => todo!("`RefHost` result not yet implemented"), WastRetCore::RefFunc(index) => match index { None => unreachable!("Expected a non-null function reference"), Some(_index) => { // use wasm::value::*; // Value::Ref(Ref::Func(FuncAddr::new(Some(index)))) - todo!("RefFuncs not yet implemented") + + return Err(GenericError::new_boxed("RefFuncs not yet implemented")); } }, - //todo!("`RefFunc` result not yet implemented"), - WastRetCore::RefAny => todo!("`RefAny` result not yet implemented"), - WastRetCore::RefEq => todo!("`RefEq` result not yet implemented"), - WastRetCore::RefArray => todo!("`RefArray` result not yet implemented"), - WastRetCore::RefStruct => todo!("`RefStruct` result not yet implemented"), - WastRetCore::RefI31 => todo!("`RefI31` result not yet implemented"), - WastRetCore::Either(_) => todo!("`Either` result not yet implemented"), + other => { + return Err(Box::new(GenericError::new(&format!( + "handling of wast ret type {other:?} not yet implemented" + )))); + } }, wast::WastRet::Component(_) => todo!("`Component` result not yet implemented"), - } + }; + + Ok(value) +} + +pub fn get_linenum(contents: &str, span: wast::token::Span) -> u32 { + span.linecol_in(contents).0 as u32 + 1 } pub fn get_command(contents: &str, span: wast::token::Span) -> &str { @@ -667,3 +601,12 @@ pub fn get_command(contents: &str, span: wast::token::Span) -> &str { .next() .unwrap_or("") } + +pub fn catch_unwind_and_suppress_panic_handler( + f: impl FnOnce() -> R + UnwindSafe, +) -> Result> { + std::panic::set_hook(Box::new(|_| {})); + let result = std::panic::catch_unwind(f); + let _ = std::panic::take_hook(); + result +} diff --git a/tests/specification/test_errors.rs b/tests/specification/test_errors.rs index 843468e255..a37c4e6b6c 100644 --- a/tests/specification/test_errors.rs +++ b/tests/specification/test_errors.rs @@ -41,22 +41,26 @@ impl AssertEqError { let left_el = left[i]; let right_el = right[i]; - if !match (left_el, right_el) { + match (left_el, right_el) { (Value::F32(a), Value::F32(b)) => { match_f32(a, b)?; - true + Ok(()) } (Value::F64(a), Value::F64(b)) => { match_f64(a, b)?; - true + Ok(()) } - (a, b) => a == b, - } { - return Err(AssertEqError { - left: format!("{:?}", left), - right: format!("{:?}", right), - }); - } + (a, b) => { + if a != b { + Err(AssertEqError { + left: format!("{:?}", left), + right: format!("{:?}", right), + }) + } else { + Ok(()) + } + } + }?; } Ok(()) } @@ -80,10 +84,10 @@ fn match_f32(actual: F32, expected: F32) -> Result<(), AssertEqError> { if (actual_bits & 0x7fff_ffff) == canon_nan { Ok(()) } else { - return Err(AssertEqError { + Err(AssertEqError { left: actual_bits.to_string(), right: canon_nan.to_string(), - }); + }) } } NanPattern::ArithmeticNan => { @@ -94,20 +98,20 @@ fn match_f32(actual: F32, expected: F32) -> Result<(), AssertEqError> { if is_nan && is_msb_set { Ok(()) } else { - return Err(AssertEqError { + Err(AssertEqError { left: actual_bits.to_string(), right: AF32_NAN.to_string(), - }); + }) } } NanPattern::Value(val) => { if actual_bits == val { Ok(()) } else { - return Err(AssertEqError { + Err(AssertEqError { left: actual_bits.to_string(), right: val.to_string(), - }); + }) } } } @@ -131,10 +135,10 @@ fn match_f64(actual: F64, expected: F64) -> Result<(), AssertEqError> { if (actual_bits & 0x7fff_ffff_ffff_ffff) == canon_nan { Ok(()) } else { - return Err(AssertEqError { + Err(AssertEqError { left: actual_bits.to_string(), right: canon_nan.to_string(), - }); + }) } } NanPattern::ArithmeticNan => { @@ -145,20 +149,20 @@ fn match_f64(actual: F64, expected: F64) -> Result<(), AssertEqError> { if is_nan && is_msb_set { Ok(()) } else { - return Err(AssertEqError { + Err(AssertEqError { left: actual_bits.to_string(), right: AF64_NAN.to_string(), - }); + }) } } NanPattern::Value(val) => { if actual_bits == val { Ok(()) } else { - return Err(AssertEqError { + Err(AssertEqError { left: actual_bits.to_string(), right: val.to_string(), - }); + }) } } } @@ -186,6 +190,18 @@ impl PanicError { message: message.to_string(), } } + + pub fn from_panic(panic: Box) -> Self { + if let Ok(msg) = panic.downcast::<&str>() { + PanicError::new(&msg) + } else { + PanicError::new("Unknown panic") + } + } + + pub fn from_panic_boxed(panic: Box) -> Box { + Box::new(Self::from_panic(panic)) + } } impl Error for PanicError {} @@ -198,6 +214,12 @@ impl std::fmt::Display for PanicError { #[derive(Debug, PartialEq, Eq, Clone)] pub struct WasmInterpreterError(pub wasm::Error); +impl WasmInterpreterError { + pub fn new_boxed(error: wasm::Error) -> Box { + Box::new(Self(error)) + } +} + impl Error for WasmInterpreterError { fn source(&self) -> Option<&(dyn Error + 'static)> { match &self.0 { @@ -220,6 +242,10 @@ impl GenericError { pub fn new(message: &str) -> Self { GenericError(message.to_string()) } + + pub fn new_boxed(message: &str) -> Box { + Box::new(Self::new(message)) + } } impl Error for GenericError {}