From e395e8caa575469531fc3cbb6637eac4330094b7 Mon Sep 17 00:00:00 2001 From: Rain Date: Sun, 17 Sep 2023 19:42:43 -0700 Subject: [PATCH] Add PathBuf Arbitrary impl with tests (#368) * Add PathBuf Arbitrary impl Followup to #362. Includes documentation. Thanks to Michael for starting work on this! Co-authored-by: Michael Dougherty * Use PathParams --------- Co-authored-by: Michael Dougherty --- proptest/src/arbitrary/_std/path.rs | 112 +++++++++++++++++++++++++++- proptest/src/lib.rs | 3 + proptest/src/path.rs | 55 ++++++++++++++ proptest/src/string.rs | 2 +- 4 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 proptest/src/path.rs diff --git a/proptest/src/arbitrary/_std/path.rs b/proptest/src/arbitrary/_std/path.rs index e7d063c1..2e71b03d 100644 --- a/proptest/src/arbitrary/_std/path.rs +++ b/proptest/src/arbitrary/_std/path.rs @@ -11,13 +11,121 @@ use std::path::*; -// TODO: Figure out PathBuf and then Box/Rc/Box. +use crate::{ + arbitrary::{SMapped, StrategyFor}, + path::PathParams, + prelude::{any, any_with, Arbitrary, Strategy}, + std_facade::{string::ToString, Arc, Box, Rc, String, Vec}, + strategy::{statics::static_map, MapInto}, +}; arbitrary!(StripPrefixError; Path::new("").strip_prefix("a").unwrap_err()); +/// A private type (not actually pub) representing the output of [`PathParams`] that can't be +/// referred to by API users. +/// +/// The goal of this type is to encapsulate the output of `PathParams`. If this layer weren't +/// present, the type of `::Strategy` would be `SMapped<(bool, Vec), +/// Self>`. This is a problem because it exposes the internal representation of `PathParams` as an +/// API. For example, if an additional parameter of randomness (e.g. another bool) were added, the +/// type of `Strategy` would change. +/// +/// With `PathParamsOutput`, the type of `Strategy` is `SMapped`, which is a +/// type that can't be named directly---only via `::Strategy`. The internal +/// representation of `PathParams` can be changed without affecting the API. +#[derive(Debug)] +pub struct PathParamsOutput { + is_absolute: bool, + components: Vec, +} + +impl Arbitrary for PathParamsOutput { + type Parameters = PathParams; + type Strategy = SMapped<(bool, Vec), Self>; + + fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { + static_map( + ( + any::(), + any_with::>(( + args.components(), + args.component_regex(), + )), + ), + |(is_absolute, components)| Self { + is_absolute, + components, + }, + ) + } +} + +/// This implementation accepts as its argument a [`PathParams`] struct. It generates either a +/// relative or an absolute path with equal probability. +/// +/// Currently, this implementation does not generate: +/// +/// * Paths that are not valid UTF-8 (this is unlikely to change) +/// * Paths with a [`PrefixComponent`](std::path::PrefixComponent) on Windows, e.g. `C:\` (this may +/// change in the future) +impl Arbitrary for PathBuf { + type Parameters = PathParams; + type Strategy = SMapped; + + fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { + static_map( + any_with::(args), + |PathParamsOutput { + is_absolute, + components, + }| { + let mut out = PathBuf::new(); + if is_absolute { + out.push(&MAIN_SEPARATOR.to_string()); + } + + for component in components { + // If a component has an embedded / (or \ on Windows), remove it from the + // string. + let component = component + .chars() + .filter(|&c| !std::path::is_separator(c)) + .collect::(); + out.push(&component); + } + + out + }, + ) + } +} + +macro_rules! dst_wrapped { + ($($w: ident),*) => { + $( + /// This implementation is identical to [the `Arbitrary` implementation for + /// `PathBuf`](trait.Arbitrary.html#impl-Arbitrary-for-PathBuf). + impl Arbitrary for $w { + type Parameters = PathParams; + type Strategy = MapInto, Self>; + + fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { + any_with::(args).prop_map_into() + } + } + )* + } +} + +dst_wrapped!(Box, Rc, Arc); + #[cfg(test)] mod test { no_panic_test!( - strip_prefix_error => StripPrefixError + strip_prefix_error => StripPrefixError, + path_buf => PathBuf, + box_path => Box, + rc_path => Rc, + arc_path => Arc ); } diff --git a/proptest/src/lib.rs b/proptest/src/lib.rs index 9cc3c77e..72b90ffb 100644 --- a/proptest/src/lib.rs +++ b/proptest/src/lib.rs @@ -86,6 +86,9 @@ pub mod test_runner; pub mod tuple; pub mod option; +#[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +pub mod path; pub mod result; pub mod sample; #[cfg(feature = "std")] diff --git a/proptest/src/path.rs b/proptest/src/path.rs new file mode 100644 index 00000000..76242817 --- /dev/null +++ b/proptest/src/path.rs @@ -0,0 +1,55 @@ +//! Strategies for generating [`PathBuf`] and related path types. +//! +//! [`PathParams`] in this module is used as the argument to the +//! [`Arbitrary`](crate::arbitrary::Arbitrary) implementation for [`PathBuf`]. + +use crate::{collection::SizeRange, string::StringParam}; + +/// Parameters for the [`Arbitrary`] implementation for [`PathBuf`]. +/// +/// By default, this generates paths with 0 to 8 components uniformly at random, each of which is a +/// default [`StringParam`]. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct PathParams { + /// The number of components in the path. + components: SizeRange, + /// The regular expression to generate individual components. + component_regex: StringParam, +} + +impl PathParams { + /// Gets the number of components in the path. + pub fn components(&self) -> SizeRange { + self.components.clone() + } + + /// Sets the number of components in the path. + pub fn with_components(mut self, components: impl Into) -> Self { + self.components = components.into(); + self + } + + /// Gets the regular expression to generate individual components. + pub fn component_regex(&self) -> StringParam { + self.component_regex + } + + /// Sets the regular expression to generate individual components. + pub fn with_component_regex( + mut self, + component_regex: impl Into, + ) -> Self { + self.component_regex = component_regex.into(); + self + } +} + +impl Default for PathParams { + fn default() -> Self { + Self { + components: (0..8).into(), + // This is the default regex for `any::()`. + component_regex: StringParam::default(), + } + } +} diff --git a/proptest/src/string.rs b/proptest/src/string.rs index 5e24f825..55666cb6 100644 --- a/proptest/src/string.rs +++ b/proptest/src/string.rs @@ -27,7 +27,7 @@ use crate::test_runner::*; /// Wraps the regex that forms the `Strategy` for `String` so that a sensible /// `Default` can be given. The default is a string of non-control characters. -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct StringParam(&'static str); impl From for &'static str {