diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index f120ac4..e5daf05 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -33,3 +33,19 @@ jobs: run: cargo test --release - name: Check all examples, binaries, etc run: cargo check --all-targets + coverage: + runs-on: ubuntu-latest + name: cargo-tarpaulin + steps: + - uses: actions/checkout@v3 + - name: Get latest version of stable Rust + run: rustup update stable + - name: Install cargo-tarpaulin + uses: taiki-e/install-action@cargo-tarpaulin + - name: Check code coverage with cargo-tarpaulin + run: make coverage + - name: Upload to codecov.io + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index fa8d85a..a706d3e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ Cargo.lock target +cobertura.xml diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b3f0363 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +# These hacks are required for the test binaries depending on the dynamic library libstd-*.so +# See: https://github.com/rust-lang/cargo/issues/4651 +# +# We also need to exclude the derive macro from coverage because it will always show as 0% by +# virtue of executing at compile-time outside the view of Tarpaulin. +coverage: + env LD_LIBRARY_PATH="$(shell rustc --print sysroot)/lib" \ + cargo-tarpaulin --workspace --all-features --out xml --exclude ethereum_ssz_derive + +.PHONY: coverage diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..bfdc987 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,8 @@ +coverage: + status: + project: + default: + informational: true + patch: + default: + informational: true diff --git a/ssz/Cargo.toml b/ssz/Cargo.toml index 934145e..dff0f89 100644 --- a/ssz/Cargo.toml +++ b/ssz/Cargo.toml @@ -20,7 +20,7 @@ ethereum_ssz_derive = { version = "0.5.4", path = "../ssz_derive" } [dependencies] alloy-primitives = "0.7.7" smallvec = { version = "1.6.1", features = ["const_generics"] } -itertools = "0.10.3" +itertools = "0.13.0" [features] arbitrary = ["alloy-primitives/arbitrary"] diff --git a/ssz/tests/tests.rs b/ssz/tests/tests.rs index 17ea623..5a71561 100644 --- a/ssz/tests/tests.rs +++ b/ssz/tests/tests.rs @@ -1,14 +1,24 @@ +use alloy_primitives::{Address, B256}; use ssz::{Decode, DecodeError, Encode}; use ssz_derive::{Decode, Encode}; +use std::num::NonZeroUsize; mod round_trip { - use alloy_primitives::B256; - use super::*; - use std::collections::BTreeMap; + use std::collections::{BTreeMap, BTreeSet}; use std::iter::FromIterator; + use std::sync::Arc; fn round_trip(items: Vec) { + assert_eq!( + ::is_ssz_fixed_len(), + ::is_ssz_fixed_len() + ); + assert_eq!( + ::ssz_fixed_len(), + ::ssz_fixed_len() + ); + for item in items { let encoded = &item.as_ssz_bytes(); assert_eq!(item.ssz_bytes_len(), encoded.len()); @@ -38,8 +48,33 @@ mod round_trip { } #[test] - fn b256() { - let items: Vec = vec![B256::ZERO, B256::from([1; 32]), B256::random()]; + fn address() { + let items: Vec
= vec![ + Address::repeat_byte(0), + Address::from([1; 20]), + Address::random(), + ]; + + round_trip(items); + } + + #[test] + fn vec_of_address() { + let items: Vec> = vec![ + vec![], + vec![ + Address::repeat_byte(0), + Address::from([1; 20]), + Address::random(), + ], + ]; + + round_trip(items); + } + + #[test] + fn h256() { + let items: Vec = vec![B256::repeat_byte(0), B256::from([1; 32]), B256::random()]; round_trip(items); } @@ -156,7 +191,7 @@ mod round_trip { round_trip(items); } - #[derive(Debug, PartialEq, Encode, Decode)] + #[derive(Debug, PartialEq, Eq, Encode, Decode)] struct VariableLen { a: u16, b: Vec, @@ -278,7 +313,7 @@ mod round_trip { round_trip(items); } - #[derive(Debug, PartialEq, Encode, Decode)] + #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Encode, Decode)] struct ThreeVariableLen { a: u16, b: Vec, @@ -388,4 +423,83 @@ mod round_trip { ]; round_trip(data); } + + #[test] + fn btree_set_fixed() { + let data = vec![BTreeSet::new(), BTreeSet::from_iter(vec![0u16, 2, 4, 6])]; + round_trip(data); + } + + #[test] + fn btree_set_variable_len() { + let data = vec![ + BTreeSet::new(), + BTreeSet::from_iter(vec![ + ThreeVariableLen { + a: 1, + b: vec![3, 5, 7], + c: vec![], + d: vec![0, 0], + }, + ThreeVariableLen { + a: 99, + b: vec![1], + c: vec![2, 3, 4, 5, 6, 7, 8, 9, 10], + d: vec![4, 5, 6, 7, 8], + }, + ThreeVariableLen { + a: 0, + b: vec![], + c: vec![], + d: vec![], + }, + ]), + ]; + round_trip(data); + } + + #[test] + fn non_zero_usize() { + let data = vec![ + NonZeroUsize::new(1).unwrap(), + NonZeroUsize::new(u16::MAX as usize).unwrap(), + NonZeroUsize::new(usize::MAX).unwrap(), + ]; + round_trip(data); + } + + #[test] + fn arc_u64() { + let data = vec![Arc::new(0u64), Arc::new(u64::MAX)]; + round_trip(data); + } + + #[test] + fn arc_vec_u64() { + let data = vec![Arc::new(vec![0u64]), Arc::new(vec![u64::MAX; 10])]; + round_trip(data); + } +} + +/// Decode tests that are expected to fail. +mod decode_fail { + use super::*; + + #[test] + fn non_zero_usize() { + let zero_bytes = 0usize.as_ssz_bytes(); + assert!(NonZeroUsize::from_ssz_bytes(&zero_bytes).is_err()); + } + + #[test] + fn hash160() { + let long_bytes = B256::repeat_byte(0xff).as_ssz_bytes(); + assert!(Address::from_ssz_bytes(&long_bytes).is_err()); + } + + #[test] + fn hash256() { + let long_bytes = vec![0xff; 257]; + assert!(B256::from_ssz_bytes(&long_bytes).is_err()); + } } diff --git a/ssz_derive/Cargo.toml b/ssz_derive/Cargo.toml index b89582c..e820718 100644 --- a/ssz_derive/Cargo.toml +++ b/ssz_derive/Cargo.toml @@ -15,10 +15,10 @@ name = "ssz_derive" proc-macro = true [dependencies] -syn = "1.0.42" +syn = "2.0.69" proc-macro2 = "1.0.23" -quote = "1.0.7" -darling = "0.13.0" +quote = "1.0.18" +darling = "0.20.9" [dev-dependencies] ethereum_ssz = { path = "../ssz" } diff --git a/ssz_derive/src/lib.rs b/ssz_derive/src/lib.rs index d6e44c0..5e01ea8 100644 --- a/ssz_derive/src/lib.rs +++ b/ssz_derive/src/lib.rs @@ -288,7 +288,11 @@ fn parse_ssz_fields( let field_opts_candidates = field .attrs .iter() - .filter(|attr| attr.path.get_ident().map_or(false, |ident| *ident == "ssz")) + .filter(|attr| { + attr.path() + .get_ident() + .map_or(false, |ident| *ident == "ssz") + }) .collect::>(); if field_opts_candidates.len() > 1 { @@ -297,10 +301,7 @@ fn parse_ssz_fields( let field_opts = field_opts_candidates .first() - .map(|attr| { - let meta = attr.parse_meta().unwrap(); - FieldOpts::from_meta(&meta).unwrap() - }) + .map(|attr| FieldOpts::from_meta(&attr.meta).unwrap()) .unwrap_or_default(); (ty, ident, field_opts)