Skip to content

Commit

Permalink
Add much higher miri coverage including for proptests.
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexhuszagh committed Sep 13, 2024
1 parent 1641a9e commit 423c00a
Show file tree
Hide file tree
Showing 21 changed files with 357 additions and 234 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added fuzzing and miri code safety analysis to our CI pipelines.
- Removed requirement of `alloc` in `no_std` ennvironments without the `write` feature.
- Make multi-digit optimizations in integer parsing optional.
- Much higher miri coverage including for proptests.

### Changed

Expand All @@ -29,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Writing `-0.0` with a leading `-`.
- Reduced binary siezes when the compact feature was enabled.
- Improved performance of integer and float parsing, particularly with small integers.
- Removed almost all unsafety in `lexical-util` and clearly documented the preconditions to use safely.
- Removed almost all unsafety in `lexical-write-integer` and clearly documented the preconditions to use safely.

### Removed

Expand Down
38 changes: 6 additions & 32 deletions lexical-parse-float/tests/api_tests.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
mod util;

use crate::util::default_proptest_config;
#[cfg(feature = "format")]
use core::num;
use lexical_parse_float::{FromLexical, FromLexicalWithOptions, Options};
Expand All @@ -13,7 +16,6 @@ use lexical_util::format::NumberFormatBuilder;
use lexical_util::format::STANDARD;
use lexical_util::num::Float;
use proptest::prelude::*;
use quickcheck::quickcheck;

#[test]
fn special_bytes_test() {
Expand Down Expand Up @@ -179,7 +181,6 @@ fn f32_radix_test() {
}

#[test]
#[cfg_attr(miri, ignore)]
fn parse_f32_test() {
let parse = move |x| f32::from_lexical_partial(x);

Expand Down Expand Up @@ -242,7 +243,6 @@ fn parse_f32_test() {
}

#[test]
#[cfg_attr(miri, ignore)]
fn parse_f64_test() {
let parse = move |x| f64::from_lexical_partial(x);

Expand Down Expand Up @@ -1250,78 +1250,68 @@ fn float_equal<F: Float>(x: F, y: F) -> bool {
}
}

quickcheck! {
#[cfg_attr(miri, ignore)]
default_quickcheck! {
fn f32_roundtrip_quickcheck(x: f32) -> bool {
let string = x.to_string();
let result = f32::from_lexical(string.as_bytes());
result.map_or(false, |y| float_equal(x, y))
}

#[cfg_attr(miri, ignore)]
fn f32_short_decimal_quickcheck(x: f32) -> bool {
let string = format!("{:.4}", x);
let actual = f32::from_lexical(string.as_bytes());
let expected = string.parse::<f32>();
actual.map_or(false, |y| expected.map_or(false, |x| float_equal(x, y)))
}

#[cfg_attr(miri, ignore)]
fn f32_long_decimal_quickcheck(x: f32) -> bool {
let string = format!("{:.100}", x);
let actual = f32::from_lexical(string.as_bytes());
let expected = string.parse::<f32>();
actual.map_or(false, |y| expected.map_or(false, |x| float_equal(x, y)))
}

#[cfg_attr(miri, ignore)]
fn f32_short_exponent_quickcheck(x: f32) -> bool {
let string = format!("{:.4e}", x);
let actual = f32::from_lexical(string.as_bytes());
let expected = string.parse::<f32>();
actual.map_or(false, |y| expected.map_or(false, |x| float_equal(x, y)))
}

#[cfg_attr(miri, ignore)]
fn f32_long_exponent_quickcheck(x: f32) -> bool {
let string = format!("{:.100e}", x);
let actual = f32::from_lexical(string.as_bytes());
let expected = string.parse::<f32>();
actual.map_or(false, |y| expected.map_or(false, |x| float_equal(x, y)))
}

#[cfg_attr(miri, ignore)]
fn f64_roundtrip_quickcheck(x: f64) -> bool {
let string = x.to_string();
let result = f64::from_lexical(string.as_bytes());
result.map_or(false, |y| float_equal(x, y))
}

#[cfg_attr(miri, ignore)]
fn f64_short_decimal_quickcheck(x: f64) -> bool {
let string = format!("{:.4}", x);
let actual = f64::from_lexical(string.as_bytes());
let expected = string.parse::<f64>();
actual.map_or(false, |y| expected.map_or(false, |x| float_equal(x, y)))
}

#[cfg_attr(miri, ignore)]
fn f64_long_decimal_quickcheck(x: f64) -> bool {
let string = format!("{:.100}", x);
let actual = f64::from_lexical(string.as_bytes());
let expected = string.parse::<f64>();
actual.map_or(false, |y| expected.map_or(false, |x| float_equal(x, y)))
}

#[cfg_attr(miri, ignore)]
fn f64_short_exponent_quickcheck(x: f64) -> bool {
let string = format!("{:.4e}", x);
let actual = f64::from_lexical(string.as_bytes());
let expected = string.parse::<f64>();
actual.map_or(false, |y| expected.map_or(false, |x| float_equal(x, y)))
}

#[cfg_attr(miri, ignore)]
fn f64_long_exponent_quickcheck(x: f64) -> bool {
let string = format!("{:.100e}", x);
let actual = f64::from_lexical(string.as_bytes());
Expand All @@ -1330,7 +1320,6 @@ quickcheck! {
}

#[cfg(feature = "f16")]
#[cfg_attr(miri, ignore)]
fn f16_roundtrip_quickcheck(bits: u16) -> bool {
let x = f16::from_bits(bits);
let string = x.as_f32().to_string();
Expand All @@ -1339,7 +1328,6 @@ quickcheck! {
}

#[cfg(feature = "f16")]
#[cfg_attr(miri, ignore)]
fn bf16_roundtrip_quickcheck(bits: u16) -> bool {
let x = bf16::from_bits(bits);
let string = x.as_f32().to_string();
Expand All @@ -1349,16 +1337,16 @@ quickcheck! {
}

proptest! {
#![proptest_config(default_proptest_config())]

#[test]
#[cfg_attr(miri, ignore)]
fn f32_invalid_proptest(i in r"[+-]?[0-9]{2}[^\deE]?\.[^\deE]?[0-9]{2}[^\deE]?e[+-]?[0-9]+[^\deE]") {
let res = f32::from_lexical(i.as_bytes());
prop_assert!(res.is_err());
prop_assert!(res.err().unwrap().is_invalid_digit());
}

#[test]
#[cfg_attr(miri, ignore)]
fn f32_double_sign_proptest(i in r"[+-]{2}[0-9]{2}\.[0-9]{2}e[+-]?[0-9]+") {
let res = f32::from_lexical(i.as_bytes());
prop_assert!(res.is_err());
Expand All @@ -1369,7 +1357,6 @@ proptest! {
}

#[test]
#[cfg_attr(miri, ignore)]
fn f32_sign_or_dot_only_proptest(i in r"[+-]?\.?") {
let res = f32::from_lexical(i.as_bytes());
prop_assert!(res.is_err());
Expand All @@ -1380,52 +1367,45 @@ proptest! {
}

#[test]
#[cfg_attr(miri, ignore)]
fn f32_double_exponent_sign_proptest(i in r"[+-]?[0-9]{2}\.[0-9]{2}e[+-]{2}[0-9]+") {
let res = f32::from_lexical(i.as_bytes());
prop_assert!(res.is_err());
prop_assert!(res.err().unwrap().is_empty_exponent());
}

#[test]
#[cfg_attr(miri, ignore)]
fn f32_missing_exponent_proptest(i in r"[+-]?[0-9]{2}\.[0-9]{2}e[+-]?") {
let res = f32::from_lexical(i.as_bytes());
prop_assert!(res.is_err());
prop_assert!(res.err().unwrap().is_empty_exponent());
}

#[test]
#[cfg_attr(miri, ignore)]
fn f32_roundtrip_display_proptest(i in f32::MIN..f32::MAX) {
let input: String = format!("{}", i);
prop_assert_eq!(i, f32::from_lexical(input.as_bytes()).unwrap());
}

#[test]
#[cfg_attr(miri, ignore)]
fn f32_roundtrip_debug_proptest(i in f32::MIN..f32::MAX) {
let input: String = format!("{:?}", i);
prop_assert_eq!(i, f32::from_lexical(input.as_bytes()).unwrap());
}

#[test]
#[cfg_attr(miri, ignore)]
fn f32_roundtrip_scientific_proptest(i in f32::MIN..f32::MAX) {
let input: String = format!("{:e}", i);
prop_assert_eq!(i, f32::from_lexical(input.as_bytes()).unwrap());
}

#[test]
#[cfg_attr(miri, ignore)]
fn f64_invalid_proptest(i in r"[+-]?[0-9]{2}[^\deE]?\.[^\deE]?[0-9]{2}[^\deE]?e[+-]?[0-9]+[^\deE]") {
let res = f64::from_lexical(i.as_bytes());
prop_assert!(res.is_err());
prop_assert!(res.err().unwrap().is_invalid_digit());
}

#[test]
#[cfg_attr(miri, ignore)]
fn f64_double_sign_proptest(i in r"[+-]{2}[0-9]{2}\.[0-9]{2}e[+-]?[0-9]+") {
let res = f64::from_lexical(i.as_bytes());
prop_assert!(res.is_err());
Expand All @@ -1436,7 +1416,6 @@ proptest! {
}

#[test]
#[cfg_attr(miri, ignore)]
fn f64_sign_or_dot_only_proptest(i in r"[+-]?\.?") {
let res = f64::from_lexical(i.as_bytes());
prop_assert!(res.is_err());
Expand All @@ -1447,37 +1426,32 @@ proptest! {
}

#[test]
#[cfg_attr(miri, ignore)]
fn f64_double_exponent_sign_proptest(i in r"[+-]?[0-9]{2}\.[0-9]{2}e[+-]{2}[0-9]+") {
let res = f64::from_lexical(i.as_bytes());
prop_assert!(res.is_err());
prop_assert!(res.err().unwrap().is_empty_exponent());
}

#[test]
#[cfg_attr(miri, ignore)]
fn f64_missing_exponent_proptest(i in r"[+-]?[0-9]{2}\.[0-9]{2}e[+-]?") {
let res = f64::from_lexical(i.as_bytes());
prop_assert!(res.is_err());
prop_assert!(res.err().unwrap().is_empty_exponent());
}

#[test]
#[cfg_attr(miri, ignore)]
fn f64_roundtrip_display_proptest(i in f64::MIN..f64::MAX) {
let input: String = format!("{}", i);
prop_assert_eq!(i, f64::from_lexical(input.as_bytes()).unwrap());
}

#[test]
#[cfg_attr(miri, ignore)]
fn f64_roundtrip_debug_proptest(i in f64::MIN..f64::MAX) {
let input: String = format!("{:?}", i);
prop_assert_eq!(i, f64::from_lexical(input.as_bytes()).unwrap());
}

#[test]
#[cfg_attr(miri, ignore)]
fn f64_roundtrip_scientific_proptest(i in f64::MIN..f64::MAX) {
let input: String = format!("{:e}", i);
prop_assert_eq!(i, f64::from_lexical(input.as_bytes()).unwrap());
Expand Down
57 changes: 57 additions & 0 deletions lexical-parse-float/tests/util.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#![allow(dead_code, unused_imports)]

use proptest::prelude::*;
pub(crate) use quickcheck::QuickCheck;

pub fn default_proptest_config() -> ProptestConfig {
ProptestConfig {
cases: if cfg!(miri) {
10
} else {
ProptestConfig::default().cases
},
max_shrink_iters: if cfg!(miri) {
10
} else {
ProptestConfig::default().max_shrink_iters
},
failure_persistence: if cfg!(miri) {
None
} else {
ProptestConfig::default().failure_persistence
},
..ProptestConfig::default()
}
}

// This is almost identical to quickcheck's itself, just to add default arguments
// https://docs.rs/quickcheck/1.0.3/src/quickcheck/lib.rs.html#43-67
// The code is unlicensed.
#[macro_export]
macro_rules! default_quickcheck {
(@as_items $($i:item)*) => ($($i)*);
{
$(
$(#[$m:meta])*
fn $fn_name:ident($($arg_name:ident : $arg_ty:ty),*) -> $ret:ty {
$($code:tt)*
}
)*
} => (
$crate::default_quickcheck! {
@as_items
$(
#[test]
$(#[$m])*
fn $fn_name() {
fn prop($($arg_name: $arg_ty),*) -> $ret {
$($code)*
}
$crate::util::QuickCheck::new()
.max_tests(if cfg!(miri) { 10 } else { 10_000 })
.quickcheck(prop as fn($($arg_ty),*) -> $ret);
}
)*
}
)
}
6 changes: 3 additions & 3 deletions lexical-parse-integer/tests/algorithm_tests.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#![cfg(not(feature = "compact"))]

#[cfg(feature = "power-of-two")]
mod util;

use crate::util::default_proptest_config;
use lexical_parse_integer::algorithm;
use lexical_parse_integer::options::SMALL_NUMBERS;
use lexical_util::format::STANDARD;
Expand Down Expand Up @@ -180,8 +180,9 @@ fn algorithm_128_test() {
}

proptest! {
#![proptest_config(default_proptest_config())]

#[test]
#[cfg_attr(miri, ignore)]
fn parse_4digits_proptest(
a in 0x30u32..0x39,
b in 0x30u32..0x39,
Expand All @@ -196,7 +197,6 @@ proptest! {
}

#[test]
#[cfg_attr(miri, ignore)]
fn parse_8digits_proptest(
a in 0x30u64..0x39,
b in 0x30u64..0x39,
Expand Down
Loading

0 comments on commit 423c00a

Please sign in to comment.