diff --git a/Cargo.lock b/Cargo.lock index 8f12880..28dcd59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,6 +88,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "aws-lc-rs" version = "1.16.2" @@ -116,6 +122,21 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitcoinvert" version = "0.1.6" @@ -127,6 +148,7 @@ dependencies = [ "exitcode", "home-config", "log", + "proptest", "regex", "reqwest", "serde", @@ -398,12 +420,28 @@ dependencies = [ "typeid", ] +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "exitcode" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -915,6 +953,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.1" @@ -956,6 +1000,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -1100,6 +1153,31 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quinn" version = "0.11.9" @@ -1200,6 +1278,15 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -1302,6 +1389,19 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.23.37" @@ -1383,6 +1483,18 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.23" @@ -1653,6 +1765,19 @@ dependencies = [ "syn", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "termtree" version = "0.5.1" @@ -1876,6 +2001,12 @@ dependencies = [ "syn", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.24" diff --git a/Cargo.toml b/Cargo.toml index d26a639..eba6d03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,3 +30,4 @@ typetag = "0.2" [dev-dependencies] assert_cmd = "2.1.2" +proptest = "1.11.0" diff --git a/src/currency/btc.rs b/src/currency/btc.rs index 25ffbc7..57a0fc2 100644 --- a/src/currency/btc.rs +++ b/src/currency/btc.rs @@ -3,7 +3,7 @@ use strum_macros::{Display, EnumString}; use crate::currency::Currency; -#[derive(Serialize, Deserialize, Debug, PartialEq, EnumString, Display)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, EnumString, Display)] #[strum(ascii_case_insensitive, serialize_all = "UPPERCASE")] pub enum BitcoinUnit { BTC, // bitcoin @@ -35,3 +35,72 @@ impl Currency for BitcoinUnit { } } } + +#[cfg(test)] +mod tests { + use super::*; + use proptest::prelude::*; + + fn arb_btc_unit() -> impl Strategy { + prop_oneof![ + Just(BitcoinUnit::BTC), + Just(BitcoinUnit::MBTC), + Just(BitcoinUnit::BITS), + Just(BitcoinUnit::SAT), + Just(BitcoinUnit::MSAT), + ] + } + + proptest! { + #[test] + fn roundtrip_btc_conversion( + amount in 1.0e-8_f64..1.0e12, + from in arb_btc_unit(), + to in arb_btc_unit(), + ) { + // Convert from -> BTC -> to -> BTC -> from + let in_btc = amount * from.btc_value(); + let in_target = in_btc / to.btc_value(); + let back_in_btc = in_target * to.btc_value(); + let back_in_from = back_in_btc / from.btc_value(); + + let relative_error = ((back_in_from - amount) / amount).abs(); + prop_assert!( + relative_error < 1.0e-10, + "Roundtrip error too large: {amount} -> {back_in_from} (error: {relative_error})" + ); + } + + #[test] + fn btc_value_is_positive(unit in arb_btc_unit()) { + prop_assert!(unit.btc_value() > 0.0); + } + + #[test] + fn conversion_preserves_order( + a in 1.0_f64..1.0e12, + b in 1.0_f64..1.0e12, + unit in arb_btc_unit(), + ) { + // If a > b in one unit, a > b in any other unit + let a_btc = a * unit.btc_value(); + let b_btc = b * unit.btc_value(); + prop_assert_eq!(a > b, a_btc > b_btc); + } + + #[test] + fn round_value_has_correct_decimal_places( + amount in 0.0_f64..1.0e8, + unit in arb_btc_unit(), + ) { + let rounded = unit.round_value(amount); + let factor = 10_f64.powi(unit.decimal_places().into()); + let check = (rounded * factor).round() / factor; + let diff = (rounded - check).abs(); + prop_assert!( + diff < 1.0e-10, + "round_value produced too many decimal places: {rounded} (expected {check})" + ); + } + } +}