Skip to content

Commit

Permalink
Add PathBuf Arbitrary impl with tests (#368)
Browse files Browse the repository at this point in the history
* Add PathBuf Arbitrary impl

Followup to #362. Includes documentation.

Thanks to Michael for starting work on this!

Co-authored-by: Michael Dougherty <maackle.d@gmail.com>

* Use PathParams

---------

Co-authored-by: Michael Dougherty <maackle.d@gmail.com>
  • Loading branch information
sunshowers and maackle committed Sep 18, 2023
1 parent fcccad0 commit e395e8c
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 3 deletions.
112 changes: 110 additions & 2 deletions proptest/src/arbitrary/_std/path.rs
Expand Up @@ -11,13 +11,121 @@

use std::path::*;

// TODO: Figure out PathBuf and then Box/Rc/Box<Path>.
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 `<PathBuf as Arbitrary>::Strategy` would be `SMapped<(bool, Vec<String>),
/// 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<PathParamsOutput, Self>`, which is a
/// type that can't be named directly---only via `<PathBuf as Arbitrary>::Strategy`. The internal
/// representation of `PathParams` can be changed without affecting the API.
#[derive(Debug)]
pub struct PathParamsOutput {
is_absolute: bool,
components: Vec<String>,
}

impl Arbitrary for PathParamsOutput {
type Parameters = PathParams;
type Strategy = SMapped<(bool, Vec<String>), Self>;

fn arbitrary_with(args: Self::Parameters) -> Self::Strategy {
static_map(
(
any::<bool>(),
any_with::<Vec<String>>((
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<PathParamsOutput, Self>;

fn arbitrary_with(args: Self::Parameters) -> Self::Strategy {
static_map(
any_with::<PathParamsOutput>(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::<String>();
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<Path> {
type Parameters = PathParams;
type Strategy = MapInto<StrategyFor<PathBuf>, Self>;

fn arbitrary_with(args: Self::Parameters) -> Self::Strategy {
any_with::<PathBuf>(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<Path>,
rc_path => Rc<Path>,
arc_path => Arc<Path>
);
}
3 changes: 3 additions & 0 deletions proptest/src/lib.rs
Expand Up @@ -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")]
Expand Down
55 changes: 55 additions & 0 deletions 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<SizeRange>) -> 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<StringParam>,
) -> 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::<String>()`.
component_regex: StringParam::default(),
}
}
}
2 changes: 1 addition & 1 deletion proptest/src/string.rs
Expand Up @@ -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<StringParam> for &'static str {
Expand Down

0 comments on commit e395e8c

Please sign in to comment.