diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..4caeb27 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,62 @@ +name: Test generators +on: [pull_request, push] + +env: + IMAGE_NAME: generators + IMAGE_PATH: /tmp/generators.tar + +jobs: + build-docker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: docker/setup-buildx-action@v2 + - uses: docker/build-push-action@v4 + with: + context: . + tags: ${{ env.IMAGE_NAME }}:test + outputs: type=docker,dest=${{ env.IMAGE_PATH }} + - uses: actions/upload-artifact@v3 + with: + name: ${{ env.IMAGE_NAME }} + path: ${{ env.IMAGE_PATH }} + + build-test-libs: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1 + with: + toolchain: 1.64.0 + default: true + - uses: actions-rs/cargo@v1 + with: + command: build + args: --package compatibility-test + - uses: actions/upload-artifact@v3 + with: + name: test-libs + path: | + target/debug/libuniffi_coverall.so + target/debug/libuniffi_coverall.a + + test: + runs-on: ubuntu-latest + needs: [build-docker, build-test-libs] + steps: + - uses: actions/checkout@v3 + - uses: docker/setup-buildx-action@v2 + - uses: actions/download-artifact@v3 + with: + name: ${{ env.IMAGE_NAME }} + path: /tmp + - uses: actions/download-artifact@v3 + with: + name: test-libs + path: target/debug + - name: Load docker image + run: | + docker load --input ${{ env.IMAGE_PATH }} + - name: Run tests + run: | + sudo ./test_generators.sh diff --git a/.gitignore b/.gitignore index de8f18a..3cf04bf 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ target/ # These are backup files generated by rustfmt **/*.rs.bk + +compatibility-test/tmp/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 81d176e..9c26be6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,6 +188,15 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "compatibility-test" +version = "0.22.0" +dependencies = [ + "once_cell", + "thiserror", + "uniffi 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "fs-err" version = "2.9.0" @@ -258,24 +267,21 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.6" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" [[package]] name = "libc" -version = "0.2.144" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "log" -version = "0.4.17" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" [[package]] name = "memchr" @@ -317,21 +323,21 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "os_str_bytes" -version = "6.5.0" +version = "6.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" +checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" [[package]] name = "paste" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" +checksum = "b4b27ab7be369122c218afc2079489cdcb4b517c0a3fc386ff11e1fedfcc2b35" [[package]] name = "plain" @@ -365,18 +371,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.27" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" dependencies = [ "proc-macro2", ] @@ -400,9 +406,9 @@ checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9" [[package]] name = "scroll" @@ -415,13 +421,13 @@ dependencies = [ [[package]] name = "scroll_derive" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdbda6ac5cd1321e724fa9cee216f3a61885889b896f073b8f82322789c5250e" +checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.23", ] [[package]] @@ -435,29 +441,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.162" +version = "1.0.166" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71b2f6e1ab5c2b98c05f0f35b236b22e8df7ead6ffbf51d7808da7f8817e7ab6" +checksum = "d01b7404f9d441d3ad40e6a636a7782c377d2abdbe4fa2440e2edcc2f4f10db8" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.162" +version = "1.0.166" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2a0814352fd64b58489904a44ea8d90cb1a91dcb6b4f5ebabc32c8318e93cb6" +checksum = "5dd83d6dde2b6b2d466e14d9d1acce8816dedee94f735eac6395808b3483c6d6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.23", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c" dependencies = [ "itoa", "ryu", @@ -501,9 +507,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.15" +version = "2.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737" dependencies = [ "proc-macro2", "quote", @@ -532,22 +538,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "c16a64ba9387ef3fdae4f9c1a7f07a0997fce91985c0336f1ddc1822b3b37802" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "d14928354b01c4d6a4f0e549069adef399a284e7995c7ccca94e8a07a5346c59" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.23", ] [[package]] @@ -570,9 +576,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" [[package]] name = "unicode-linebreak" @@ -590,6 +596,18 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "uniffi" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f71cc01459bc34cfe43fabf32b39f1228709bc6db1b3a664a92940af3d062376" +dependencies = [ + "anyhow", + "uniffi_build 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)", + "uniffi_core 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)", + "uniffi_macros 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "uniffi" version = "0.23.0" @@ -598,16 +616,40 @@ dependencies = [ "anyhow", "camino", "clap", - "uniffi_bindgen", - "uniffi_core", - "uniffi_macros", + "uniffi_bindgen 0.23.0 (git+https://github.com/NordSecurity/uniffi-rs.git?rev=a4318f3b55a7ee2c12eb9f0fe1b5122d89323fc4)", + "uniffi_core 0.23.0 (git+https://github.com/NordSecurity/uniffi-rs.git?rev=a4318f3b55a7ee2c12eb9f0fe1b5122d89323fc4)", + "uniffi_macros 0.23.0 (git+https://github.com/NordSecurity/uniffi-rs.git?rev=a4318f3b55a7ee2c12eb9f0fe1b5122d89323fc4)", ] [[package]] name = "uniffi-bindgen" version = "0.23.0" dependencies = [ - "uniffi", + "uniffi 0.23.0 (git+https://github.com/NordSecurity/uniffi-rs.git?rev=a4318f3b55a7ee2c12eb9f0fe1b5122d89323fc4)", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbbba5103051c18f10b22f80a74439ddf7100273f217a547005d2735b2498994" +dependencies = [ + "anyhow", + "askama", + "bincode", + "camino", + "fs-err", + "glob", + "goblin", + "heck", + "once_cell", + "paste", + "serde", + "serde_json", + "toml", + "uniffi_meta 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)", + "uniffi_testing 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)", + "weedle2 4.0.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -629,9 +671,20 @@ dependencies = [ "serde_json", "textwrap", "toml", - "uniffi_meta", - "uniffi_testing", - "weedle2", + "uniffi_meta 0.23.0 (git+https://github.com/NordSecurity/uniffi-rs.git?rev=a4318f3b55a7ee2c12eb9f0fe1b5122d89323fc4)", + "uniffi_testing 0.23.0 (git+https://github.com/NordSecurity/uniffi-rs.git?rev=a4318f3b55a7ee2c12eb9f0fe1b5122d89323fc4)", + "weedle2 4.0.0 (git+https://github.com/NordSecurity/uniffi-rs.git?rev=a4318f3b55a7ee2c12eb9f0fe1b5122d89323fc4)", +] + +[[package]] +name = "uniffi_build" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1a28368ff3d83717e3d3e2e15a66269c43488c3f036914131bb68892f29fb" +dependencies = [ + "anyhow", + "camino", + "uniffi_bindgen 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -641,7 +694,17 @@ source = "git+https://github.com/NordSecurity/uniffi-rs.git?rev=a4318f3b55a7ee2c dependencies = [ "anyhow", "camino", - "uniffi_bindgen", + "uniffi_bindgen 0.23.0 (git+https://github.com/NordSecurity/uniffi-rs.git?rev=a4318f3b55a7ee2c12eb9f0fe1b5122d89323fc4)", +] + +[[package]] +name = "uniffi_checksum_derive" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03de61393a42b4ad4984a3763c0600594ac3e57e5aaa1d05cede933958987c03" +dependencies = [ + "quote", + "syn 1.0.109", ] [[package]] @@ -653,6 +716,22 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "uniffi_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2b4852d638d74ca2d70e450475efb6d91fe6d54a7cd8d6bd80ad2ee6cd7daa" +dependencies = [ + "anyhow", + "bytes", + "camino", + "cargo_metadata", + "log", + "once_cell", + "paste", + "static_assertions", +] + [[package]] name = "uniffi_core" version = "0.23.0" @@ -668,6 +747,25 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "uniffi_macros" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa03394de21e759e0022f1ea8d992d2e39290d735b9ed52b1f74b20a684f794e" +dependencies = [ + "bincode", + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn 1.0.109", + "toml", + "uniffi_build 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)", + "uniffi_meta 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "uniffi_macros" version = "0.23.0" @@ -682,8 +780,19 @@ dependencies = [ "serde", "syn 1.0.109", "toml", - "uniffi_build", - "uniffi_meta", + "uniffi_build 0.23.0 (git+https://github.com/NordSecurity/uniffi-rs.git?rev=a4318f3b55a7ee2c12eb9f0fe1b5122d89323fc4)", + "uniffi_meta 0.23.0 (git+https://github.com/NordSecurity/uniffi-rs.git?rev=a4318f3b55a7ee2c12eb9f0fe1b5122d89323fc4)", +] + +[[package]] +name = "uniffi_meta" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fdab2c436aed7a6391bec64204ec33948bfed9b11b303235740771f85c4ea6" +dependencies = [ + "serde", + "siphasher", + "uniffi_checksum_derive 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -693,7 +802,22 @@ source = "git+https://github.com/NordSecurity/uniffi-rs.git?rev=a4318f3b55a7ee2c dependencies = [ "serde", "siphasher", - "uniffi_checksum_derive", + "uniffi_checksum_derive 0.23.0 (git+https://github.com/NordSecurity/uniffi-rs.git?rev=a4318f3b55a7ee2c12eb9f0fe1b5122d89323fc4)", +] + +[[package]] +name = "uniffi_testing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92b0570953ec41d97ce23e3b92161ac18231670a1f97523258a6d2ab76d7f76c" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "fs-err", + "once_cell", + "serde", + "serde_json", ] [[package]] @@ -722,6 +846,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "weedle2" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e79c5206e1f43a2306fd64bdb95025ee4228960f2e6c5a8b173f3caaf807741" +dependencies = [ + "nom", +] + [[package]] name = "weedle2" version = "4.0.0" diff --git a/Cargo.toml b/Cargo.toml index 3ddac44..e657f02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,6 @@ -[package] -name = "uniffi-bindgen" -version = "0.23.0" -edition = "2021" +[workspace] -[dependencies] -uniffi = { git = "https://github.com/NordSecurity/uniffi-rs.git", rev = "a4318f3b55a7ee2c12eb9f0fe1b5122d89323fc4", features = ["cli"] } +members = [ + "uniffi-bindgen", + "compatibility-test" +] \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 7a7dfe8..c984b77 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ FROM rust:1.64-bullseye as builder COPY . /project -RUN cd /project && cargo install --path . +RUN cd /project && cargo install --path uniffi-bindgen RUN cargo install uniffi-bindgen-cs --tag v0.2.3+v0.23.0 --git https://github.com/NordSecurity/uniffi-bindgen-cs -RUN cargo install uniffi-bindgen-go --tag v0.1.2+v0.23.0 --git https://github.com/NordSecurity/uniffi-bindgen-go +RUN cargo install uniffi-bindgen-go --tag v0.1.3+v0.23.0 --git https://github.com/NordSecurity/uniffi-bindgen-go FROM debian:bullseye COPY --from=builder /usr/local/cargo/bin/uniffi-bindgen /bin diff --git a/README.md b/README.md index 95cb5d6..53cd828 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ shows which versions of each generator are inside the docker image. | Docker image | uniffi-rs version | uniffi-bindgen-cs version | uniffi-bindgen-go version | |------------------------|-------------------|---------------------------|---------------------------| -| v0.23.0-4 | v0.23.0 (FORK) | v0.2.3+v0.23.0 | v0.1.1+v0.23.0 | +| v0.23.0-4 | v0.23.0 (FORK) | v0.2.3+v0.23.0 | v0.1.3+v0.23.0 | | v0.23.0-3 (DO NOT USE) | v0.23.0 (FORK) | v0.2.2+v0.23.0 | v0.1.0+v0.23.0 | | v0.23.0-2 (DO NOT USE) | v0.23.0 | v0.2.1+v0.23.0 | v0.1.0+v0.23.0 | | v0.23.0-1 | v0.23.0 | v0.2.1+v0.23.0 | not present | diff --git a/compatibility-test/Cargo.toml b/compatibility-test/Cargo.toml new file mode 100644 index 0000000..86eb8c2 --- /dev/null +++ b/compatibility-test/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "compatibility-test" +version = "0.22.0" +authors = ["Firefox Sync Team "] +edition = "2021" +license = "MPL-2.0" +publish = false + +[lib] +crate-type = ["lib", "staticlib", "cdylib"] +name = "uniffi_coverall" + +[dependencies] +uniffi = { version = "0.23.0" } +once_cell = "1.12" +thiserror = "1.0" + +[build-dependencies] +uniffi = { version = "0.23.0", features = ["build"] } diff --git a/compatibility-test/build.rs b/compatibility-test/build.rs new file mode 100644 index 0000000..85e7a67 --- /dev/null +++ b/compatibility-test/build.rs @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +fn main() { + uniffi::generate_scaffolding("./src/coverall.udl").unwrap(); +} diff --git a/compatibility-test/src/coverall.udl b/compatibility-test/src/coverall.udl new file mode 100644 index 0000000..9f12676 --- /dev/null +++ b/compatibility-test/src/coverall.udl @@ -0,0 +1,152 @@ +namespace coverall { + SimpleDict create_some_dict(); + SimpleDict create_none_dict(); + + u64 get_num_alive(); + + // void returning error throwing namespace function to catch clippy warnings (eg, #1330) + [Throws=CoverallError] + void println(string text); +}; + +dictionary SimpleDict { + string text; + string? maybe_text; + boolean a_bool; + boolean? maybe_a_bool; + u8 unsigned8; + u8? maybe_unsigned8; + u16 unsigned16; + u16? maybe_unsigned16; + u64 unsigned64; + u64? maybe_unsigned64; + i8 signed8; + i8? maybe_signed8; + i64 signed64; + i64? maybe_signed64; + float float32; + float? maybe_float32; + double float64; + double? maybe_float64; + Coveralls? coveralls; +}; + +dictionary DictWithDefaults { + string name = "default-value"; + string? category = null; + u64 integer = 31; +}; + +[Enum] +interface MaybeSimpleDict { + Yeah(SimpleDict d); + Nah(); +}; + +[Error] +enum CoverallError { + "TooManyHoles" +}; + +[Error] +interface ComplexError { + OsError(i16 code, i16 extended_code); + PermissionDenied(string reason); +}; + +interface Coveralls { + constructor(string name); + + // Either constructs a new object or throws an error. + [Throws=CoverallError, Name="fallible_new"] + constructor(string name, boolean should_fail); + + // Always panics, just to test panics in ctors are handled. + [Name="panicking_new"] + constructor(string message); + + string get_name(); + + [Throws=CoverallError] + boolean maybe_throw(boolean should_throw); + + /// Throws something that impls `Into`, + /// rather than directly throwing `CoverallError`. + [Throws=CoverallError] + boolean maybe_throw_into(boolean should_throw); + + [Throws=ComplexError] + boolean maybe_throw_complex(i8 input); + + void panic(string message); + + [Throws=CoverallError] + void fallible_panic(string message); + + // *** Test functions which take either `self` or other params as `Arc` *** + + /// Calls `Arc::strong_count()` on the `Arc` containing `self`. + [Self=ByArc] + u64 strong_count(); + + /// Takes an `Arc` and stores it in `self`, dropping the existing + /// reference. Note you can create circular references by passing `self`. + void take_other(Coveralls? other); + + /// Returns what was previously set via `take_other()`, or null. + Coveralls? get_other(); + + /// Same signature as `take_other` but always fails. + [Self=ByArc, Throws=CoverallError] + void take_other_fallible(); + + /// Same signature as `take_other` but with an extra string arg - always + /// panics with that message.. + [Self=ByArc] + void take_other_panic(string message); + + // can't name it `clone` as it conflicts with the Clone trait and ours has a different signature + Coveralls clone_me(); + + // regression test: using a parameter name that was also used by UniFFI runtime code + string get_status(string status); + + /// Simple string->integer dictionary, using the legacy `DOMString` type. + record get_dict(string key, u64 value); + + /// Simple string->integer dictionary, using the classic string type + record get_dict2(string key, u64 value); + + /// integer->integer dictionary + record get_dict3(u32 key, u64 value); + + /// Adds a new repair at the current time. + void add_patch(Patch patch); + + /// Adds a new repair at the specified time. + void add_repair(Repair repair); + + /// Returns all repairs made. + sequence get_repairs(); +}; + +// coveralls keep track of their repairs (an interface in a dict) +dictionary Repair { + timestamp when; + Patch patch; +}; + +// All coveralls end up with a patch. +enum Color {"Red", "Blue", "Green"}; + +interface Patch { + constructor(Color color); + + Color get_color(); +}; + +interface ThreadsafeCounter { + constructor(); + void busy_wait(i32 ms); + i32 increment_if_busy(); +}; diff --git a/compatibility-test/src/lib.rs b/compatibility-test/src/lib.rs new file mode 100644 index 0000000..ad30703 --- /dev/null +++ b/compatibility-test/src/lib.rs @@ -0,0 +1,353 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; +use std::sync::{Arc, Mutex, RwLock}; +use std::time::SystemTime; + +use once_cell::sync::Lazy; + +static NUM_ALIVE: Lazy> = Lazy::new(|| RwLock::new(0)); + +#[derive(Debug, thiserror::Error)] +pub enum CoverallError { + #[error("The coverall has too many holes")] + TooManyHoles, +} + +/// This error doesn't appear in the interface, instead +/// we rely on an `Into` impl to surface it to consumers. +#[derive(Debug, thiserror::Error)] +pub enum InternalCoverallError { + #[error("The coverall has an excess of holes")] + ExcessiveHoles, +} + +impl From for CoverallError { + fn from(err: InternalCoverallError) -> CoverallError { + match err { + InternalCoverallError::ExcessiveHoles => CoverallError::TooManyHoles, + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum ComplexError { + #[error("OsError: {code} ({extended_code})")] + OsError { code: i16, extended_code: i16 }, + #[error("PermissionDenied: {reason}")] + PermissionDenied { reason: String }, +} + +#[derive(Debug, Clone)] +pub struct SimpleDict { + text: String, + maybe_text: Option, + a_bool: bool, + maybe_a_bool: Option, + unsigned8: u8, + maybe_unsigned8: Option, + unsigned16: u16, + maybe_unsigned16: Option, + unsigned64: u64, + maybe_unsigned64: Option, + signed8: i8, + maybe_signed8: Option, + signed64: i64, + maybe_signed64: Option, + float32: f32, + maybe_float32: Option, + float64: f64, + maybe_float64: Option, + coveralls: Option>, +} + +#[derive(Debug, Clone)] +pub struct DictWithDefaults { + name: String, + category: Option, + integer: u64, +} + +#[derive(Debug, Clone)] +pub enum MaybeSimpleDict { + Yeah { d: SimpleDict }, + Nah, +} + +fn create_some_dict() -> SimpleDict { + SimpleDict { + text: "text".to_string(), + maybe_text: Some("maybe_text".to_string()), + a_bool: true, + maybe_a_bool: Some(false), + unsigned8: 1, + maybe_unsigned8: Some(2), + unsigned16: 3, + maybe_unsigned16: Some(4), + unsigned64: u64::MAX, + maybe_unsigned64: Some(u64::MIN), + signed8: 8, + maybe_signed8: Some(0), + signed64: i64::MAX, + maybe_signed64: Some(0), + float32: 1.2345, + maybe_float32: Some(22.0 / 7.0), + float64: 0.0, + maybe_float64: Some(1.0), + coveralls: Some(Arc::new(Coveralls::new("some_dict".to_string()))), + } +} + +fn create_none_dict() -> SimpleDict { + SimpleDict { + text: "text".to_string(), + maybe_text: None, + a_bool: true, + maybe_a_bool: None, + unsigned8: 1, + maybe_unsigned8: None, + unsigned16: 3, + maybe_unsigned16: None, + unsigned64: u64::MAX, + maybe_unsigned64: None, + signed8: 8, + maybe_signed8: None, + signed64: i64::MAX, + maybe_signed64: None, + float32: 1.2345, + maybe_float32: None, + float64: 0.0, + maybe_float64: None, + coveralls: None, + } +} + +fn get_num_alive() -> u64 { + *NUM_ALIVE.read().unwrap() +} + +type Result = std::result::Result; +type ComplexResult = std::result::Result; + +fn println(text: String) -> Result<()> { + println!("coveralls println: {text}"); + Ok(()) +} + +#[derive(Debug)] +pub struct Coveralls { + name: String, + // A reference to another Coveralls. Currently will be only a reference + // to `self`, so will create a circular reference. + other: Mutex>>, + // Repairs we've made to this coverall. + repairs: Mutex>, +} + +impl Coveralls { + fn new(name: String) -> Self { + *NUM_ALIVE.write().unwrap() += 1; + Self { + name, + other: Mutex::new(None), + repairs: Mutex::new(Vec::new()), + } + } + + fn fallible_new(name: String, should_fail: bool) -> Result { + if should_fail { + Err(CoverallError::TooManyHoles) + } else { + Ok(Self::new(name)) + } + } + + fn fallible_panic(&self, message: String) -> Result<()> { + panic!("{message}"); + } + + fn get_name(&self) -> String { + self.name.clone() + } + + fn panicking_new(message: String) -> Self { + panic!("{message}"); + } + + fn maybe_throw(&self, should_throw: bool) -> Result { + if should_throw { + Err(CoverallError::TooManyHoles) + } else { + Ok(true) + } + } + + fn maybe_throw_into(&self, should_throw: bool) -> Result { + if should_throw { + Err(InternalCoverallError::ExcessiveHoles) + } else { + Ok(true) + } + } + + fn maybe_throw_complex(&self, input: i8) -> ComplexResult { + match input { + 0 => Ok(true), + 1 => Err(ComplexError::OsError { + code: 10, + extended_code: 20, + }), + 2 => Err(ComplexError::PermissionDenied { + reason: "Forbidden".to_owned(), + }), + _ => panic!("Invalid input"), + } + } + + fn panic(&self, message: String) { + panic!("{message}"); + } + + fn strong_count(self: Arc) -> u64 { + Arc::strong_count(&self) as u64 + } + + fn take_other(&self, other: Option>) { + *self.other.lock().unwrap() = other.map(|arc| Arc::clone(&arc)) + } + + fn get_other(&self) -> Option> { + (*self.other.lock().unwrap()).as_ref().map(Arc::clone) + } + + fn take_other_fallible(self: Arc) -> Result<()> { + Err(CoverallError::TooManyHoles) + } + + fn take_other_panic(self: Arc, message: String) { + panic!("{message}"); + } + + fn clone_me(&self) -> Arc { + let other = self.other.lock().unwrap(); + let new_other = Mutex::new(other.clone()); + *NUM_ALIVE.write().unwrap() += 1; + Arc::new(Self { + name: self.name.clone(), + other: new_other, + repairs: Mutex::new(Vec::new()), + }) + } + + fn get_status(&self, status: String) -> String { + format!("status: {status}") + } + + fn get_dict(&self, key: String, value: u64) -> HashMap { + let mut map = HashMap::new(); + map.insert(key, value); + map + } + + fn get_dict2(&self, key: String, value: u64) -> HashMap { + let mut map = HashMap::new(); + map.insert(key, value); + map + } + + fn get_dict3(&self, key: u32, value: u64) -> HashMap { + let mut map = HashMap::new(); + map.insert(key, value); + map + } + + fn add_patch(&self, patch: Arc) { + let repair = Repair { + when: SystemTime::now(), + patch, + }; + let mut repairs = self.repairs.lock().unwrap(); + repairs.push(repair); + } + + fn add_repair(&self, repair: Repair) { + let mut repairs = self.repairs.lock().unwrap(); + repairs.push(repair); + } + + fn get_repairs(&self) -> Vec { + let repairs = self.repairs.lock().unwrap(); + repairs.clone() + } +} + +impl Drop for Coveralls { + fn drop(&mut self) { + *NUM_ALIVE.write().unwrap() -= 1; + } +} + +#[derive(Debug, Clone)] +pub struct Repair { + when: SystemTime, + patch: Arc, +} + +#[derive(Debug, Clone, Copy)] +pub enum Color { + Red, + Blue, + Green, +} + +#[derive(Debug, Clone)] +struct Patch { + color: Color, +} + +impl Patch { + fn new(color: Color) -> Self { + Self { color } + } + + fn get_color(&self) -> Color { + self.color + } +} + +// This is a small implementation of a counter that allows waiting on one thread, +// and counting on another thread. We use it to test that the UniFFI generated scaffolding +// doesn't introduce unexpected locking behaviour between threads. +struct ThreadsafeCounter { + is_busy: AtomicBool, + count: AtomicI32, +} + +impl ThreadsafeCounter { + fn new() -> Self { + Self { + is_busy: AtomicBool::new(false), + count: AtomicI32::new(0), + } + } + + fn busy_wait(&self, ms: i32) { + self.is_busy.store(true, Ordering::SeqCst); + // Pretend to do some work in a blocking fashion. + std::thread::sleep(std::time::Duration::from_millis(ms as u64)); + self.is_busy.store(false, Ordering::SeqCst); + } + + fn increment_if_busy(&self) -> i32 { + if self.is_busy.load(Ordering::SeqCst) { + self.count.fetch_add(1, Ordering::SeqCst) + 1 + } else { + self.count.load(Ordering::SeqCst) + } + } +} + +uniffi::include_scaffolding!("coverall"); \ No newline at end of file diff --git a/compatibility-test/tests/cs/dotnet.csproj b/compatibility-test/tests/cs/dotnet.csproj new file mode 100644 index 0000000..ea69a12 --- /dev/null +++ b/compatibility-test/tests/cs/dotnet.csproj @@ -0,0 +1,22 @@ + + + net6.0 + enable + enable + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/compatibility-test/tests/cs/test_coverall.cs b/compatibility-test/tests/cs/test_coverall.cs new file mode 100644 index 0000000..d38e3b9 --- /dev/null +++ b/compatibility-test/tests/cs/test_coverall.cs @@ -0,0 +1,167 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +using Xunit; +using System; +using uniffi.coverall; + +public class TestCoverall { + [Fact] + public void FFIObjectSafeHandleDropsNativeReferenceOutsideOfUsingBlock() { + Assert.Equal(0UL, CoverallMethods.GetNumAlive()); + var closure = () => { + var coveralls = new Coveralls("safe_handle_drops_native_reference"); + Assert.Equal(1UL, CoverallMethods.GetNumAlive()); + }; + closure(); + GC.Collect(); + Thread.Sleep(1); + Assert.Equal(0UL, CoverallMethods.GetNumAlive()); + } + + [Fact] + public void TestCreateSomeDict() { + using (var d = CoverallMethods.CreateSomeDict()) { + Assert.Equal("text", d.text); + Assert.Equal("maybe_text", d.maybeText); + Assert.True(d.aBool); + Assert.False(d.maybeABool); + Assert.Equal((byte)1, d.unsigned8); + Assert.Equal((byte)2, d.maybeUnsigned8); + Assert.Equal((ushort)3, d.unsigned16); + Assert.Equal((ushort)4, d.maybeUnsigned16); + Assert.Equal(18446744073709551615UL, d.unsigned64); + Assert.Equal(0ul, d.maybeUnsigned64); + Assert.Equal((sbyte)8, d.signed8); + Assert.Equal((sbyte)0, d.maybeSigned8); + Assert.Equal(9223372036854775807L, d.signed64); + Assert.Equal(0L, d.maybeSigned64); + + Assert.Equal(1.2345f, d.float32); + Assert.Equal(22.0f / 7.0f, d.maybeFloat32); + Assert.Equal(0.0, d.float64); + Assert.Equal(1.0, d.maybeFloat64); + + Assert.Equal("some_dict", d.coveralls!.GetName()); + } + } + + [Fact] + public void TestArcs() { + using (var coveralls = new Coveralls("test_arcs")) { + Assert.Equal(1UL, CoverallMethods.GetNumAlive()); + // One ref held by the foreign-language code, one created for this method call. + Assert.Equal(2UL, coveralls.StrongCount()); + Assert.Null(coveralls.GetOther()); + coveralls.TakeOther(coveralls); + // Should now be a new strong ref, held by the object's reference to itself. + Assert.Equal(3UL, coveralls.StrongCount()); + Assert.Equal(1UL, CoverallMethods.GetNumAlive()); + // Careful, this makes a new C# object which must be separately destroyed. + using (var other = coveralls.GetOther()) { + Assert.Equal("test_arcs", other!.GetName()); + } + + Assert.Throws(() => coveralls.TakeOtherFallible()); + + Assert.Throws(() => coveralls.TakeOtherPanic("expected panic: with an arc!")); + + Assert.Throws(() => coveralls.FalliblePanic("Expected panic in a fallible function!")); + + coveralls.TakeOther(null); + Assert.Equal(2UL, coveralls.StrongCount()); + } + + Assert.Equal(0UL, CoverallMethods.GetNumAlive()); + } + + [Fact] + public void TestReturnObjects() { + using (var coveralls = new Coveralls("test_return_objects")) { + Assert.Equal(1UL, CoverallMethods.GetNumAlive()); + Assert.Equal(2UL, coveralls.StrongCount()); + using (var c2 = coveralls.CloneMe()) { + Assert.Equal(coveralls.GetName(), c2.GetName()); + Assert.Equal(2UL, CoverallMethods.GetNumAlive()); + Assert.Equal(2UL, c2.StrongCount()); + + coveralls.TakeOther(c2); + // same number alive but `c2` has an additional ref count. + Assert.Equal(2UL, CoverallMethods.GetNumAlive()); + Assert.Equal(2UL, coveralls.StrongCount()); + Assert.Equal(3UL, c2.StrongCount()); + } + + // Here we've dropped C# reference to `c2`, but the rust struct will not + // be dropped as coveralls hold an `Arc<>` to it. + Assert.Equal(2UL, CoverallMethods.GetNumAlive()); + } + + Assert.Equal(0UL, CoverallMethods.GetNumAlive()); + } + + [Fact] + public void TestSimpleErrors() { + using (var coveralls = new Coveralls("test_simple_errors")) { + Assert.Throws(() => coveralls.MaybeThrow(true)); + Assert.Throws(() => coveralls.MaybeThrowInto(true)); + Assert.Throws(() => coveralls.Panic("oops")); + } + } + + [Fact] + public void TestComplexErrors() { + using (var coveralls = new Coveralls("test_complex_errors")) { + Assert.True(coveralls.MaybeThrowComplex(0)); + + var os_exception = Assert.Throws( + () => coveralls.MaybeThrowComplex(1)); + Assert.Equal(10, os_exception.code); + Assert.Equal(20, os_exception.extendedCode); + + var permission_denied = Assert.Throws( + () => coveralls.MaybeThrowComplex(2)); + Assert.Equal("Forbidden", permission_denied.reason); + + Assert.Throws(() => coveralls.MaybeThrowComplex(3)); + } + } + + [Fact] + public void TestInterfacesInDicts() { + using (var coveralls = new Coveralls("test_interface_in_dicts")) { + coveralls.AddPatch(new Patch(Color.RED)); + coveralls.AddRepair(new Repair(DateTime.Now, new Patch(Color.BLUE))); + Assert.Equal(2, coveralls.GetRepairs().Count); + } + } + + [Fact] + public void MultiThreadedCallsWork() { + // Make sure that there is no blocking during concurrent FFI calls. + + using (var counter = new ThreadsafeCounter()) { + Thread blockingThread = new Thread(new ThreadStart(() => { + // block the thread for 100ms + counter.BusyWait(300); + })); + + var count = 0; + Thread countingThread = new Thread(new ThreadStart(() => { + for (int i = 0; i < 100; i++) { + // `count` is only incremented if another thread is blocking the counter. + // This ensures that both calls are running concurrently. + count = counter.IncrementIfBusy(); + } + })); + + blockingThread.Start(); + countingThread.Start(); + blockingThread.Join(); + countingThread.Join(); + Assert.True(count > 0); + } + } + +} diff --git a/compatibility-test/tests/go/coverall_test.go b/compatibility-test/tests/go/coverall_test.go new file mode 100644 index 0000000..e842501 --- /dev/null +++ b/compatibility-test/tests/go/coverall_test.go @@ -0,0 +1,188 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + package go_tests + + import ( + "testing" + "time" + + "test_coverall/uniffi/coverall" + "github.com/stretchr/testify/assert" + ) + + func TestCoverall(t *testing.T) { + d := coverall.CreateSomeDict() + defer d.Destroy() + + assert.Equal(t, "text", d.Text) + assert.Equal(t, "maybe_text", *d.MaybeText) + assert.True(t, d.ABool) + assert.False(t, *d.MaybeABool) + assert.Equal(t, uint8(1), d.Unsigned8) + assert.Equal(t, uint8(2), *d.MaybeUnsigned8) + + assert.Equal(t, uint16(3), d.Unsigned16) + assert.Equal(t, uint16(4), *d.MaybeUnsigned16) + assert.Equal(t, uint64(18446744073709551615), d.Unsigned64) + assert.Equal(t, uint64(0), *d.MaybeUnsigned64) + assert.Equal(t, int8(8), d.Signed8) + assert.Equal(t, int8(0), *d.MaybeSigned8) + assert.Equal(t, int64(9223372036854775807), d.Signed64) + assert.Equal(t, int64(0), *d.MaybeSigned64) + + assert.Equal(t, float32(1.2345), d.Float32) + assert.Equal(t, float32(22.0/7.0), *d.MaybeFloat32) + assert.Equal(t, 0.0, d.Float64) + assert.Equal(t, 1.0, *d.MaybeFloat64) + + assert.Equal(t, "some_dict", (*d.Coveralls).GetName()) + } + + func TestCoverallArcs(t *testing.T) { + coveralls := coverall.NewCoveralls("test_arcs") + + assert.Equal(t, uint64(1), coverall.GetNumAlive()) + // One ref held by the foreign-language code, one created for this method call. + assert.Equal(t, uint64(2), coveralls.StrongCount()) + assert.Nil(t, coveralls.GetOther()) + coveralls.TakeOther(&coveralls) + // Should now be a new strong ref, held by the object's reference to itself. + assert.Equal(t, uint64(3), coveralls.StrongCount()) + assert.Equal(t, uint64(1), coverall.GetNumAlive()) + + other := *coveralls.GetOther() + assert.Equal(t, "test_arcs", (*other).GetName()) + other.Destroy() + + assert.ErrorIs(t, coveralls.TakeOtherFallible(), coverall.ErrCoverallErrorTooManyHoles) + + assert.PanicsWithError(t, "expected panic: with an arc!", func() { + coveralls.TakeOtherPanic("expected panic: with an arc!") + }) + + assert.PanicsWithError(t, "Expected panic in a fallible function!", func() { + coveralls.FalliblePanic("Expected panic in a fallible function!") + }) + + coveralls.TakeOther(nil) + assert.Equal(t, uint64(2), coveralls.StrongCount()) + + coveralls.Destroy() + assert.Equal(t, uint64(0), coverall.GetNumAlive()) + } + + func TestCoverallReturnObjects(t *testing.T) { + coveralls := coverall.NewCoveralls("test_return_objects") + assert.Equal(t, uint64(1), coverall.GetNumAlive()) + assert.Equal(t, uint64(2), coveralls.StrongCount()) + + c2 := coveralls.CloneMe() + assert.Equal(t, coveralls.GetName(), c2.GetName()) + assert.Equal(t, uint64(2), coverall.GetNumAlive()) + assert.Equal(t, uint64(2), c2.StrongCount()) + + coveralls.TakeOther(&c2) + // same number alive but `c2` has an additional ref count. + assert.Equal(t, uint64(2), coverall.GetNumAlive()) + assert.Equal(t, uint64(2), coveralls.StrongCount()) + assert.Equal(t, uint64(3), c2.StrongCount()) + + c2.Destroy() + // Here we've dropped C# reference to `c2`, but the rust struct will not + // be dropped as coveralls hold an `Arc<>` to it. + assert.Equal(t, uint64(2), coverall.GetNumAlive()) + + coveralls.Destroy() + assert.Equal(t, uint64(0), coverall.GetNumAlive()) + } + + func TestCoverallSimpleErrors(t *testing.T) { + coveralls := coverall.NewCoveralls("test_simple_errors") + defer coveralls.Destroy() + + _, err := coveralls.MaybeThrow(true) + assert.ErrorIs(t, err, coverall.ErrCoverallErrorTooManyHoles) + + _, err = coveralls.MaybeThrowInto(true) + assert.ErrorIs(t, err, coverall.ErrCoverallErrorTooManyHoles) + + assert.PanicsWithError(t, "oops", func() { + coveralls.Panic("oops") + }) + } + + func TestCoverallComplexErrors(t *testing.T) { + coveralls := coverall.NewCoveralls("test_complex_errors") + defer coveralls.Destroy() + + { + v, err := coveralls.MaybeThrowComplex(0) + if assert.NoError(t, err) { + assert.True(t, v) + } + } + + { + _, err := coveralls.MaybeThrowComplex(1) + var osErr *coverall.ComplexErrorOsError + if assert.ErrorAs(t, err, &osErr) { + assert.Equal(t, int16(10), osErr.Code) + assert.Equal(t, int16(20), osErr.ExtendedCode) + } + } + + { + _, err := coveralls.MaybeThrowComplex(2) + var permissionErr *coverall.ComplexErrorPermissionDenied + if assert.ErrorAs(t, err, &permissionErr) { + assert.Equal(t, "Forbidden", permissionErr.Reason) + } + } + + assert.PanicsWithError(t, "Invalid input", func() { + coveralls.MaybeThrowComplex(3) + }) + } + + func TestCoverallInterfacesInDicts(t *testing.T) { + coveralls := coverall.NewCoveralls("test_interface_in_dicts") + defer coveralls.Destroy() + + coveralls.AddPatch(coverall.NewPatch(coverall.ColorRed)) + coveralls.AddRepair(coverall.Repair{time.Now(), coverall.NewPatch(coverall.ColorBlue)}) + assert.Equal(t, 2, len(coveralls.GetRepairs())) + } + + func TestCoverallMultiThreadedCallsWork(t *testing.T) { + // Make sure that there is no blocking during concurrent FFI calls. + + counter := coverall.NewThreadsafeCounter() + defer counter.Destroy() + + const waitMillis = 10 + + finished1 := make(chan struct{}) + go func() { + // block the thread + counter.BusyWait(waitMillis) + finished1 <- struct{}{} + }() + + count := int32(0) + finished2 := make(chan struct{}) + go func() { + for i := 0; i < waitMillis; i++ { + // `count` is only incremented if another thread is blocking the counter. + // This ensures that both calls are running concurrently. + count = counter.IncrementIfBusy() + time.Sleep(time.Millisecond) + } + finished2 <- struct{}{} + }() + + <-finished1 + <-finished2 + assert.True(t, count > 0) + } diff --git a/compatibility-test/tests/go/go.mod b/compatibility-test/tests/go/go.mod new file mode 100644 index 0000000..6622331 --- /dev/null +++ b/compatibility-test/tests/go/go.mod @@ -0,0 +1,11 @@ +module test_coverall + +go 1.19 + +require github.com/stretchr/testify v1.8.1 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/compatibility-test/tests/go/go.sum b/compatibility-test/tests/go/go.sum new file mode 100644 index 0000000..dc5d0e0 --- /dev/null +++ b/compatibility-test/tests/go/go.sum @@ -0,0 +1,16 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/compatibility-test/tests/kotlin/test_coverall.kts b/compatibility-test/tests/kotlin/test_coverall.kts new file mode 100644 index 0000000..34f85c8 --- /dev/null +++ b/compatibility-test/tests/kotlin/test_coverall.kts @@ -0,0 +1,218 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import java.time.Instant +import java.util.concurrent.* + +import uniffi.coverall.* + +// TODO: use an actual test runner. + +// Test some_dict(). +// N.B. we need to `use` here to clean up the contained `Coveralls` reference. +createSomeDict().use { d -> + assert(d.text == "text"); + assert(d.maybeText == "maybe_text"); + assert(d.aBool); + assert(d.maybeABool == false); + assert(d.unsigned8 == 1.toUByte()) + assert(d.maybeUnsigned8 == 2.toUByte()) + assert(d.unsigned16 == 3.toUShort()) + assert(d.maybeUnsigned16 == 4.toUShort()) + assert(d.unsigned64 == 18446744073709551615UL) + assert(d.maybeUnsigned64 == 0UL) + assert(d.signed8 == 8.toByte()) + assert(d.maybeSigned8 == 0.toByte()) + assert(d.signed64 == 9223372036854775807L) + assert(d.maybeSigned64 == 0L) + + // floats should be "close enough". + fun Float.almostEquals(other: Float) = Math.abs(this - other) < 0.000001 + fun Double.almostEquals(other: Double) = Math.abs(this - other) < 0.000001 + + assert(d.float32.almostEquals(1.2345F)) + assert(d.maybeFloat32!!.almostEquals(22.0F/7.0F)) + assert(d.float64.almostEquals(0.0)) + assert(d.maybeFloat64!!.almostEquals(1.0)) + + assert(d.coveralls!!.getName() == "some_dict") +} + + +// Test arcs. + +Coveralls("test_arcs").use { coveralls -> + assert(getNumAlive() == 1UL); + // One ref held by the foreign-language code, one created for this method call. + assert(coveralls.strongCount() == 2UL); + assert(coveralls.getOther() == null); + coveralls.takeOther(coveralls); + // Should now be a new strong ref, held by the object's reference to itself. + assert(coveralls.strongCount() == 3UL); + // But the same number of instances. + assert(getNumAlive() == 1UL); + // Careful, this makes a new Kotlin object which must be separately destroyed. + coveralls.getOther()!!.use { other -> + // It's the same Rust object. + assert(other.getName() == "test_arcs") + } + try { + coveralls.takeOtherFallible() + throw RuntimeException("Should have thrown an IntegerOverflow exception!") + } catch (e: CoverallException.TooManyHoles) { + // It's okay! + } + try { + coveralls.takeOtherPanic("expected panic: with an arc!") + throw RuntimeException("Should have thrown an InternalException!") + } catch (e: InternalException) { + // No problemo! + } + + try { + coveralls.falliblePanic("Expected panic in a fallible function!") + throw RuntimeException("Should have thrown an InternalException") + } catch (e: InternalException) { + // No problemo! + } + coveralls.takeOther(null); + assert(coveralls.strongCount() == 2UL); +} +assert(getNumAlive() == 0UL); + +// Test return objects + +Coveralls("test_return_objects").use { coveralls -> + assert(getNumAlive() == 1UL) + assert(coveralls.strongCount() == 2UL) + coveralls.cloneMe().use { c2 -> + assert(c2.getName() == coveralls.getName()) + assert(getNumAlive() == 2UL) + assert(c2.strongCount() == 2UL) + + coveralls.takeOther(c2) + // same number alive but `c2` has an additional ref count. + assert(getNumAlive() == 2UL) + assert(coveralls.strongCount() == 2UL) + assert(c2.strongCount() == 3UL) + } + // Here we've dropped Kotlin's reference to `c2`, but the rust struct will not + // be dropped as coveralls hold an `Arc<>` to it. + assert(getNumAlive() == 2UL) +} +// Destroying `coveralls` will kill both. +assert(getNumAlive() == 0UL); + +Coveralls("test_simple_errors").use { coveralls -> + try { + coveralls.maybeThrow(true) + throw RuntimeException("Expected method to throw exception") + } catch(e: CoverallException.TooManyHoles) { + // Expected result + assert(e.message == "The coverall has too many holes") + } + + try { + coveralls.maybeThrowInto(true) + throw RuntimeException("Expected method to throw exception") + } catch(e: CoverallException.TooManyHoles) { + // Expected result + } + + try { + coveralls.panic("oops") + throw RuntimeException("Expected method to throw exception") + } catch(e: InternalException) { + // Expected result + assert(e.message == "oops") + } +} + +Coveralls("test_complex_errors").use { coveralls -> + assert(coveralls.maybeThrowComplex(0) == true) + + try { + coveralls.maybeThrowComplex(1) + throw RuntimeException("Expected method to throw exception") + } catch(e: ComplexException.OsException) { + assert(e.code == 10.toShort()) + assert(e.extendedCode == 20.toShort()) + assert(e.toString() == "uniffi.coverall.ComplexException\$OsException: code=10, extendedCode=20") { + "Unexpected ComplexError.OsError.toString() value: ${e.toString()}" + } + } + + try { + coveralls.maybeThrowComplex(2) + throw RuntimeException("Expected method to throw exception") + } catch(e: ComplexException.PermissionDenied) { + assert(e.reason == "Forbidden") + assert(e.toString() == "uniffi.coverall.ComplexException\$PermissionDenied: reason=Forbidden") { + "Unexpected ComplexError.PermissionDenied.toString() value: ${e.toString()}" + } + } + + try { + coveralls.maybeThrowComplex(3) + throw RuntimeException("Expected method to throw exception") + } catch(e: InternalException) { + // Expected result + } +} + +Coveralls("test_interfaces_in_dicts").use { coveralls -> + coveralls.addPatch(Patch(Color.RED)) + coveralls.addRepair( + Repair(`when`=Instant.now(), patch=Patch(Color.BLUE)) + ) + assert(coveralls.getRepairs().size == 2) +} + +Coveralls("test_regressions").use { coveralls -> + assert(coveralls.getStatus("success") == "status: success") +} + +// This tests that the UniFFI-generated scaffolding doesn't introduce any unexpected locking. +// We have one thread busy-wait for a some period of time, while a second thread repeatedly +// increments the counter and then checks if the object is still busy. The second thread should +// not be blocked on the first, and should reliably observe the first thread being busy. +// If it does not, that suggests UniFFI is accidentally serializing the two threads on access +// to the shared counter object. + +ThreadsafeCounter().use { counter -> + val executor = Executors.newFixedThreadPool(3) + try { + val busyWaiting: Future = executor.submit(Callable { + // 300 ms should be long enough for the other thread to easily finish + // its loop, but not so long as to annoy the user with a slow test. + counter.busyWait(300) + }) + val incrementing: Future = executor.submit(Callable { + var count = 0 + for (n in 1..100) { + // We exect most iterations of this loop to run concurrently + // with the busy-waiting thread. + count = counter.incrementIfBusy() + } + count + }) + + busyWaiting.get() + val count = incrementing.get() + assert(count > 0) { "Counter doing the locking: incrementIfBusy=$count" } + } finally { + executor.shutdown() + } +} + +// This does not call Rust code. +var d = DictWithDefaults() +assert(d.name == "default-value") +assert(d.category == null) +assert(d.integer == 31UL) + +d = DictWithDefaults(name = "this", category = "that", integer = 42UL) +assert(d.name == "this") +assert(d.category == "that") +assert(d.integer == 42UL) diff --git a/compatibility-test/tests/python/test_coverall.py b/compatibility-test/tests/python/test_coverall.py new file mode 100644 index 0000000..a8fd0c8 --- /dev/null +++ b/compatibility-test/tests/python/test_coverall.py @@ -0,0 +1,216 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import unittest +from datetime import datetime, timezone +from coverall import * + +class TestCoverall(unittest.TestCase): + # Any test not terminating with zero objects alive will cause others to + # fail - this helps us work out which test kept things alive. + def tearDown(self): + self.assertEqual(get_num_alive(), 0) + + def test_some_dict(self): + d = create_some_dict() + self.assertEqual(d.text, "text") + self.assertEqual(d.maybe_text, "maybe_text") + self.assertTrue(d.a_bool) + self.assertFalse(d.maybe_a_bool) + self.assertEqual(d.unsigned8, 1) + self.assertEqual(d.maybe_unsigned8, 2) + self.assertEqual(d.unsigned16, 3) + self.assertEqual(d.maybe_unsigned16, 4) + self.assertEqual(d.unsigned64, 18446744073709551615) + self.assertEqual(d.maybe_unsigned64, 0) + self.assertEqual(d.signed8, 8) + self.assertEqual(d.maybe_signed8, 0) + self.assertEqual(d.signed64, 9223372036854775807) + self.assertEqual(d.maybe_signed64, 0) + self.assertEqual(d.coveralls.get_name(), "some_dict") + + # floats should be "close enough" - although it's mildly surprising that + # we need to specify `places=6` whereas the default is 7. + self.assertAlmostEqual(d.float32, 1.2345, places=6) + self.assertAlmostEqual(d.maybe_float32, 22.0/7.0, places=6) + self.assertAlmostEqual(d.float64, 0.0) + self.assertAlmostEqual(d.maybe_float64, 1.0) + + def test_none_dict(self): + d = create_none_dict() + self.assertEqual(d.text, "text") + self.assertIsNone(d.maybe_text) + self.assertTrue(d.a_bool) + self.assertIsNone(d.maybe_a_bool) + self.assertEqual(d.unsigned8, 1) + self.assertIsNone(d.maybe_unsigned8) + self.assertEqual(d.unsigned16, 3) + self.assertIsNone(d.maybe_unsigned16) + self.assertEqual(d.unsigned64, 18446744073709551615) + self.assertIsNone(d.maybe_unsigned64) + self.assertEqual(d.signed8, 8) + self.assertIsNone(d.maybe_signed8) + self.assertEqual(d.signed64, 9223372036854775807) + self.assertIsNone(d.maybe_signed64) + + self.assertAlmostEqual(d.float32, 1.2345, places=6) + self.assertIsNone(d.maybe_float32) + self.assertAlmostEqual(d.float64, 0.0) + self.assertIsNone(d.maybe_float64) + self.assertIsNone(d.coveralls) + + def test_constructors(self): + self.assertEqual(get_num_alive(), 0) + # must work. + coveralls = Coveralls("c1") + self.assertEqual(get_num_alive(), 1) + # make sure it really is our Coveralls object. + self.assertEqual(coveralls.get_name(), "c1") + # must also work. + coveralls2 = Coveralls.fallible_new("c2", False) + self.assertEqual(get_num_alive(), 2) + # make sure it really is our Coveralls object. + self.assertEqual(coveralls2.get_name(), "c2") + + with self.assertRaises(CoverallError.TooManyHoles): + Coveralls.fallible_new("", True) + + with self.assertRaisesRegex(InternalError, "expected panic: woe is me"): + Coveralls.panicking_new("expected panic: woe is me") + + # in the absence of cycles Python is deterministic in killing refs + coveralls2 = None + self.assertEqual(get_num_alive(), 1) + coveralls = None + self.assertEqual(get_num_alive(), 0) + + + def test_simple_errors(self): + coveralls = Coveralls("test_errors") + self.assertEqual(coveralls.get_name(), "test_errors") + + with self.assertRaisesRegex(CoverallError.TooManyHoles, "The coverall has too many holes"): + coveralls.maybe_throw(True) + + with self.assertRaises(CoverallError.TooManyHoles): + coveralls.maybe_throw_into(True) + + with self.assertRaisesRegex(InternalError, "expected panic: oh no"): + coveralls.panic("expected panic: oh no") + + def test_complex_errors(self): + coveralls = Coveralls("test_complex_errors") + + # Test success + self.assertEqual(True, coveralls.maybe_throw_complex(0)) + + # Test errors + with self.assertRaises(ComplexError.OsError) as cm: + coveralls.maybe_throw_complex(1) + self.assertEqual(cm.exception.code, 10) + self.assertEqual(cm.exception.extended_code, 20) + self.assertEqual(str(cm.exception), "ComplexError.OsError(code=10, extended_code=20)") + + with self.assertRaises(ComplexError.PermissionDenied) as cm: + coveralls.maybe_throw_complex(2) + self.assertEqual(cm.exception.reason, "Forbidden") + self.assertEqual(str(cm.exception), "ComplexError.PermissionDenied(reason='Forbidden')") + + # Test panics, which should cause InternalError to be raised + with self.assertRaises(InternalError) as cm: + coveralls.maybe_throw_complex(3) + + def test_self_by_arc(self): + coveralls = Coveralls("test_self_by_arc") + # One reference is held by the handlemap, and one by the `Arc` method receiver. + self.assertEqual(coveralls.strong_count(), 2) + + def test_arcs(self): + coveralls = Coveralls("test_arcs") + self.assertEqual(get_num_alive(), 1) + self.assertEqual(coveralls.strong_count(), 2) + self.assertIsNone(coveralls.get_other()) + coveralls.take_other(coveralls) + # should now be a new strong ref. + self.assertEqual(coveralls.strong_count(), 3) + # but the same number of instances. + self.assertEqual(get_num_alive(), 1) + # and check it's the correct object. + self.assertEqual(coveralls.get_other().get_name(), "test_arcs") + + with self.assertRaises(CoverallError.TooManyHoles): + coveralls.take_other_fallible() + + with self.assertRaisesRegex(InternalError, "expected panic: with an arc!"): + coveralls.take_other_panic("expected panic: with an arc!") + + coveralls.take_other(None) + self.assertEqual(coveralls.strong_count(), 2) + coveralls = None + self.assertEqual(get_num_alive(), 0) + + def test_return_objects(self): + coveralls = Coveralls("test_return_objects") + self.assertEqual(get_num_alive(), 1) + self.assertEqual(coveralls.strong_count(), 2) + c2 = coveralls.clone_me() + self.assertEqual(c2.get_name(), coveralls.get_name()) + self.assertEqual(get_num_alive(), 2) + self.assertEqual(c2.strong_count(), 2) + + coveralls.take_other(c2) + # same number alive but `c2` has an additional ref count. + self.assertEqual(get_num_alive(), 2) + self.assertEqual(coveralls.strong_count(), 2) + self.assertEqual(c2.strong_count(), 3) + + # We can drop Python's reference to `c2`, but the rust struct will not + # be dropped as coveralls hold an `Arc<>` to it. + c2 = None + self.assertEqual(get_num_alive(), 2) + + coveralls.add_patch(Patch(Color.RED)) + coveralls.add_repair( + Repair(when=datetime.now(timezone.utc), patch=Patch(Color.BLUE)) + ) + self.assertEqual(len(coveralls.get_repairs()), 2) + + # Dropping `coveralls` will kill both. + coveralls = None + self.assertEqual(get_num_alive(), 0) + + def test_bad_objects(self): + coveralls = Coveralls("test_bad_objects") + patch = Patch(Color.RED) + # `coveralls.take_other` wants `Coveralls` not `Patch` + with self.assertRaisesRegex(TypeError, "Coveralls.*Patch"): + coveralls.take_other(patch) + + def test_dict_with_defaults(self): + """ This does not call Rust code. """ + + d = DictWithDefaults() + self.assertEqual("default-value", d.name) + self.assertEqual(None, d.category) + self.assertEqual(31, d.integer) + + d = DictWithDefaults(name="this", category="that", integer=42) + self.assertEqual("this", d.name) + self.assertEqual("that", d.category) + self.assertEqual(42, d.integer) + + def test_dict_with_non_string_keys(self): + coveralls = Coveralls("test_dict") + + dict1 = coveralls.get_dict(key="answer", value=42) + assert dict1["answer"] == 42 + + dict2 = coveralls.get_dict2(key="answer", value=42) + assert dict2["answer"] == 42 + + dict3 = coveralls.get_dict3(key=31, value=42) + assert dict3[31] == 42 + +if __name__=='__main__': + unittest.main() diff --git a/compatibility-test/tests/swift/test_coverall.swift b/compatibility-test/tests/swift/test_coverall.swift new file mode 100644 index 0000000..e24103a --- /dev/null +++ b/compatibility-test/tests/swift/test_coverall.swift @@ -0,0 +1,204 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import Foundation +import coverall + +// TODO: use an actual test runner. + + +// Floats should be "close enough" for testing purposes. +fileprivate extension Double { + func almostEquals(_ other: Double) -> Bool { + return abs(self - other) < 0.000001 + } +} + +fileprivate extension Float { + func almostEquals(_ other: Float) -> Bool { + return abs(self - other) < 0.000001 + } +} + +// Test some_dict(). +do { + let d = createSomeDict() + assert(d.text == "text") + assert(d.maybeText == "maybe_text") + assert(d.aBool) + assert(d.maybeABool == false); + assert(d.unsigned8 == 1) + assert(d.maybeUnsigned8 == 2) + assert(d.unsigned16 == 3) + assert(d.maybeUnsigned16 == 4) + assert(d.unsigned64 == 18446744073709551615) + assert(d.maybeUnsigned64 == 0) + assert(d.signed8 == 8) + assert(d.maybeSigned8 == 0) + assert(d.signed64 == 9223372036854775807) + assert(d.maybeSigned64 == 0) + assert(d.float32.almostEquals(1.2345)) + assert(d.maybeFloat32!.almostEquals(22.0/7.0)) + assert(d.float64.almostEquals(0.0)) + assert(d.maybeFloat64!.almostEquals(1.0)) + assert(d.coveralls!.getName() == "some_dict") +} + +// Test arcs. +do { + let coveralls = Coveralls(name: "test_arcs") + assert(getNumAlive() == 1) + // One ref held by the foreign-language code, one created for this method call. + assert(coveralls.strongCount() == 2) + assert(coveralls.getOther() == nil) + coveralls.takeOther(other: coveralls) + // Should now be a new strong ref, held by the object's reference to itself. + assert(coveralls.strongCount() == 3) + // But the same number of instances. + assert(getNumAlive() == 1) + // It's the same Rust object. + assert(coveralls.getOther()!.getName() == "test_arcs") + do { + try coveralls.takeOtherFallible() + fatalError("Should have thrown") + } catch CoverallError.TooManyHoles { + // It's okay! + } + // TODO: kinda hard to test this, as it triggers a fatal error. + // coveralls!.takeOtherPanic(message: "expected panic: with an arc!") + // do { + // try coveralls.falliblePanic(message: "Expected Panic!!") + // } catch CoverallError.TooManyHoles { + // fatalError("Should have panicked!") + // } + coveralls.takeOther(other: nil); + assert(coveralls.strongCount() == 2); +} + +// Test simple errors +do { + let coveralls = Coveralls(name: "test_simple_errors") + + assert(try! coveralls.maybeThrow(shouldThrow: false) == true) + + do { + let _ = try coveralls.maybeThrow(shouldThrow: true) + fatalError("Should have thrown") + } catch CoverallError.TooManyHoles(let message) { + // It's okay! + assert(message == "The coverall has too many holes") + } + + do { + let _ = try coveralls.maybeThrowInto(shouldThrow: true) + fatalError("Should have thrown") + } catch CoverallError.TooManyHoles { + // It's okay! + } + + // Note: Can't test coveralls.panic() because rust panics trigger a fatal error in swift +} + +// Test complex errors +do { + let coveralls = Coveralls(name: "test_complex_errors") + + assert(try! coveralls.maybeThrowComplex(input: 0) == true) + + do { + let _ = try coveralls.maybeThrowComplex(input: 1) + fatalError("should have thrown") + } catch let e as ComplexError { + if case let .OsError(code, extendedCode) = e { + assert(code == 10) + assert(extendedCode == 20) + } else { + fatalError("wrong error variant: \(e)") + } + assert(String(describing: e) == "OsError(code: 10, extendedCode: 20)", "Unexpected ComplexError.OsError description: \(e)") + } + + do { + let _ = try coveralls.maybeThrowComplex(input: 2) + fatalError("should have thrown") + } catch let e as ComplexError { + if case let .PermissionDenied(reason) = e { + assert(reason == "Forbidden") + } else { + fatalError("wrong error variant: \(e)") + } + assert(String(describing: e) == "PermissionDenied(reason: \"Forbidden\")", "Unexpected ComplexError.PermissionDenied description: \(e)") + } + + do { + let _ = try coveralls.maybeThrowComplex(input: 3) + fatalError("should have thrown") + } catch { + assert(String(describing: error) == "rustPanic(\"Invalid input\")") + } + +} + +// Swift GC is deterministic, `coveralls` is freed when it goes out of scope. +assert(getNumAlive() == 0); + +// Test return objects +do { + let coveralls = Coveralls(name: "test_return_objects") + assert(getNumAlive() == 1) + assert(coveralls.strongCount() == 2) + do { + let c2 = coveralls.cloneMe() + assert(c2.getName() == coveralls.getName()) + assert(getNumAlive() == 2) + assert(c2.strongCount() == 2) + + coveralls.takeOther(other: c2) + // same number alive but `c2` has an additional ref count. + assert(getNumAlive() == 2) + assert(coveralls.strongCount() == 2) + assert(c2.strongCount() == 3) + } + // We can drop Swifts's reference to `c2`, but the rust struct will not + // be dropped as coveralls hold an `Arc<>` to it. + assert(getNumAlive() == 2) +} + +// Dropping `coveralls` will kill both. +assert(getNumAlive() == 0) + +// Test a dict with defaults +// This does not call Rust code. +do { + let d = DictWithDefaults() + assert(d.name == "default-value") + assert(d.category == nil) + assert(d.integer == 31) + + let d2 = DictWithDefaults(name: "this", category: "that", integer: 42) + assert(d2.name == "this") + assert(d2.category == "that") + assert(d2.integer == 42) +} + +do { + let coveralls = Coveralls(name: "test_dicts") + + let dict1 = coveralls.getDict(key: "answer", value: 42) + assert(dict1["answer"] == 42) + + let dict2 = coveralls.getDict2(key: "answer", value: 42) + assert(dict2["answer"] == 42) + + let dict3 = coveralls.getDict3(key: 31, value: 42) + assert(dict3[31] == 42) +} + +// Test interfaces as dict members +do { + let coveralls = Coveralls(name: "test_interfaces_in_dicts") + coveralls.addPatch(patch: Patch(color: Color.red)) + coveralls.addRepair(repair: Repair(when: Date.init(), patch: Patch(color: Color.blue))) + assert(coveralls.getRepairs().count == 2) +} diff --git a/compatibility-test/uniffi.toml b/compatibility-test/uniffi.toml new file mode 100644 index 0000000..8af8c34 --- /dev/null +++ b/compatibility-test/uniffi.toml @@ -0,0 +1,2 @@ +[bindings.csharp] +cdylib_name = "uniffi_coverall" diff --git a/test_generators.sh b/test_generators.sh new file mode 100755 index 0000000..68115a1 --- /dev/null +++ b/test_generators.sh @@ -0,0 +1,175 @@ +#!/bin/bash +set -euo pipefail + + +# Run compatibility tests for all generators +# You can either run this script with -bd flags +# to build the test library and docker image +# or you can build them manually and then run this script. +# To build them manually you need to: +# 1. Build test library in debug mode by running: +# cargo build --package compatibility-test +# in the root of this repo +# 2. Build the docker image with generators by running: +# docker build -t generators:test . +# in the root of this repo +# +# NOTE: +# Building library with this script by adding -b +# is beneficial because the built library will be +# compatibile with docker images used to run tests. +# It is possible that the version built locally might +# be not compatible e.g. when the version of GLIBC on +# your system is newer than the one in the docker image. + + +if [[ $EUID -ne 0 ]]; then + echo "$0 must be run as root. Otherwise there is a problem with permissions of files created inside docker containers. Try using sudo." + exit 1 +fi + +usage() { echo -e "Usage: $0 [-b] [-d]\n -b - build test library\n -d - build docker image" 1>&2; exit 1; } + +BUILD_LIBS=0 +BUILD_DOCKER=0 + +while getopts "bd" o; do + case "${o}" in + b) + BUILD_LIBS=1 + ;; + d) + BUILD_DOCKER=1 + ;; + *) + usage + ;; + esac +done + +SCRIPT_DIR="${SCRIPT_DIR:-$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )}" +ROOT_DIR="$SCRIPT_DIR" +TESTS_DIR="$ROOT_DIR/compatibility-test/tests" +TMP_DIR="$ROOT_DIR/compatibility-test/tmp" + +# Build test library: +function build_docker() { + docker run --rm \ + -v $ROOT_DIR:/workspace \ + -w /workspace \ + rust:1.64.0-slim-bullseye \ + $* +} + +if [ $BUILD_LIBS -eq 1 ]; then + build_docker cargo build --package compatibility-test +fi + + +# Build docker image with generators: +if [ $BUILD_DOCKER -eq 1 ]; then + docker build -t generators:test . +fi + + +# Generate bindings for all languages: +function bindings_docker() { + docker run --rm \ + -v $ROOT_DIR:/workspace \ + -w /workspace/compatibility-test \ + generators:test \ + $* +} + +UDL_FILE="src/coverall.udl" +bindings_docker uniffi-bindgen generate $UDL_FILE --language python --out-dir "tmp/python" +bindings_docker uniffi-bindgen generate $UDL_FILE --language kotlin --out-dir "tmp/kotlin" +bindings_docker uniffi-bindgen generate $UDL_FILE --language swift --out-dir "tmp/swift" +bindings_docker uniffi-bindgen-cs $UDL_FILE --out-dir "tmp/cs" --config="uniffi.toml" +bindings_docker uniffi-bindgen-go $UDL_FILE --out-dir "tmp/go" + + +# Python tests: +function python_docker() { + docker run --rm \ + -v $ROOT_DIR:/workspace \ + -w /workspace/compatibility-test/tmp/python \ + python:3.10-slim-bookworm \ + $* +} + +cp $TESTS_DIR/python/* $TMP_DIR/python +cp $ROOT_DIR/target/debug/libuniffi_coverall.so $TMP_DIR/python +python_docker python3 -m unittest discover -v + + +# Kotlin tests: +function kotlin_docker() { + docker run --rm \ + -v $ROOT_DIR:/workspace \ + -w /workspace/compatibility-test/tmp/kotlin \ + -e LD_LIBRARY_PATH=/workspace/target/debug \ + schlaubiboy/kotlin:1.9.0-jdk16-debian \ + $* +} + +cp $TESTS_DIR/kotlin/* $TMP_DIR/kotlin +curl -L "https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.7.0/jna-5.7.0.jar" -o $TMP_DIR/kotlin/jna.jar +kotlin_docker kotlinc -Werror -d coverall.jar uniffi/coverall/coverall.kt -classpath ".:jna.jar" +kotlin_docker kotlinc -Werror -classpath ".:jna.jar:coverall.jar" -J-ea -script test_coverall.kts + + +# Swift tests: +function swift_docker() { + docker run --rm \ + -v $ROOT_DIR:/workspace \ + -w /workspace/compatibility-test/tmp/swift \ + -e LD_LIBRARY_PATH=/workspace/target/debug \ + swift:5.7 \ + $* +} + +cp $TESTS_DIR/swift/* $TMP_DIR/swift +pushd $TMP_DIR/swift +mkdir -p coverallFFI +cp coverallFFI.h coverallFFI/ +cp coverallFFI.modulemap coverallFFI/module.modulemap +popd + +swift_docker swiftc -v -emit-module -module-name coverall -o libtestmod.so -emit-library -Xcc -fmodule-map-file=coverallFFI/module.modulemap -I . -L /workspace/target/debug -luniffi_coverall coverall.swift +swift_docker swift -I . -L . -ltestmod -luniffi_coverall -Xcc -fmodule-map-file=coverallFFI/module.modulemap test_coverall.swift + + +# C# tests: +function cs_docker() { + docker run --rm \ + -v $ROOT_DIR:/workspace \ + -w /workspace/compatibility-test/tmp/cs \ + -e LD_LIBRARY_PATH=/workspace/target/debug \ + mcr.microsoft.com/dotnet/sdk:6.0-jammy \ + $* +} + +cp $TESTS_DIR/cs/* $TMP_DIR/cs +cs_docker dotnet test -l "console;verbosity=normal" + + +# Go tests: +function go_docker() { + docker run --rm \ + -v $ROOT_DIR:/workspace \ + -w /workspace/compatibility-test/tmp/go \ + -e CGO_ENABLED=1 \ + -e CGO_LDFLAGS="-luniffi_coverall -L/workspace/target/debug -ldl -lm" \ + -e LD_LIBRARY_PATH=/workspace/target/debug \ + golang:1.20 \ + $* +} + +cp $TESTS_DIR/go/* $TMP_DIR/go +go_docker go test -v + + +# Clean up: +rm -rf $TMP_DIR +echo Compatibility tests passed! diff --git a/uniffi-bindgen/Cargo.toml b/uniffi-bindgen/Cargo.toml new file mode 100644 index 0000000..3ddac44 --- /dev/null +++ b/uniffi-bindgen/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "uniffi-bindgen" +version = "0.23.0" +edition = "2021" + +[dependencies] +uniffi = { git = "https://github.com/NordSecurity/uniffi-rs.git", rev = "a4318f3b55a7ee2c12eb9f0fe1b5122d89323fc4", features = ["cli"] } diff --git a/src/main.rs b/uniffi-bindgen/src/main.rs similarity index 100% rename from src/main.rs rename to uniffi-bindgen/src/main.rs