-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Simplifying property configuration #346
base: master
Are you sure you want to change the base?
Simplifying property configuration #346
Conversation
…ithTests, and withConfig
@@ -71,91 +71,46 @@ type PropertyExtensions private () = | |||
let (Property property) = property | |||
Property.report property | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might actually be nice to keep the config
overloads. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should only expose a single way to configure properties; this new API should be the only API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not exactly sure what is being discussed here. However, I am not overly concerned with the C# API.
let ofGen (x : Gen<Journal * Outcome<'a>>) : Property<'a> = | ||
Property x | ||
let ofGen (gen : Gen<Journal * Outcome<'a>>) : Property<'a> = | ||
Property (PropertyConfig.defaultConfig, gen) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I gave the PropertyConfig.defaultConfig
zero thought.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll need to audit the use of this function, if it's being used as prop |> toGen |> doGenThing |> ofGen
, then we'll lose the original configuration.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed. All such calls should be replaced with Property.Generator.map
(as I structured/named it in another comment).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about, it a PR before this one, we change Property
from a single case discriminated union to a record with a single field as well as expressing all maps over that field as such? Then I think it would easier to verify where PropertyConfig.defaultConfig
should be used.
Property (config, gen) | ||
|
||
/// Restores the default shrinking behavior. | ||
let withoutShrinks (Property (config, gen) : Property<'a>) : Property<'a> = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be called withDefaultShrinks
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm okay with this, the current name withoutShrinks
sounds as if no shrinking will occur.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a better name that withDefaultShrinks
(which is a syntactic name) is withUnlimitedShrinking
(which is a semantic name)
|> Report.render | ||
/// Set the number of times a property is allowed to shrink before the test | ||
/// runner gives up and displays the counterexample. | ||
let withShrinks (shrinkLimit : int<shrinks>) (Property (config, gen) : Property<'a>) : Property<'a> = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be nice to just take an int
instead of an int<shrinks>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you expand on this?
@@ -4,15 +4,15 @@ open System | |||
|
|||
[<Struct>] | |||
type Property<'a> = | |||
| Property of Gen<Journal * Outcome<'a>> | |||
| Property of PropertyConfig * Gen<Journal * Outcome<'a>> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about making this into a record? How about this?
{ Config: PropertyConfig
Generator: Gen<Journal * Outcome<'a>> }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm okay with this, I think we should also mark the internals as private
, and use lower camel case for the names:
// unsure of the validity of this syntax, just typing into the comment editor right now
type Property<'a> =
private
{
config: PropertyConfig
generator: Gen<Journal * Outcome<'a>>
}
In any case, we should make 2 accessor functions, and only use them. They can be marked as inline
if we have performance concerns.
let inline config (property : Property<'a>) : PropertyConfig =
property.config
// This already exists as 'toGen'
let inline generator (property : Property<'a>) : PropertyConfig =
property.generator
Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am ok with removing the public access scope. However, I think the access scope should be internal
instead of private.
I think we should follow the F# naming guidelines and use Pascal case for record field names.
The most common operation is mapping, not (just) getting. As such, I think we should create a submodule inside Property like this (@dharmaturtle knows what I mean):
module Config =
let get p = p.Config
let set p c = { p with Config = c }
let map f p = { p with Config = f p.Config }
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me just say, I love the work done here. So many extra methods/functions are now gone! 🚀
We'll need to look at this carefully, so the review may take a while but I have no doubt that this is the right way to go. Thank you very much for doing this @dharmaturtle!
let report = | ||
property { | ||
let! actual = Range.linear 1 1_000_000 |> Gen.int | ||
return actual < 500_000 | ||
} |> Property.reportWith propConfig | ||
} |> Property.withShrinks shrinkLimit |> Property.report |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These two |>
should be on separate lines for readability.
[<Extension>] | ||
static member WithConfig (property : Property, config : PropertyConfig) : Property = | ||
let (Property property) = property | ||
Property (Property.withConfig config property) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can get rid of this, and only expose configuration with Property.with*
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the same as Property.Config.set
(as I strucutured/named it in another comment). I think it is better to keep. Might want to consider renaming to setConfig
though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would the value be in overriding a user's preferences?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be the user calling this function, it would be their preference. For example, @dharmaturtle talks about how he runs PB tests with a time limit during development and with a test limit during integration. I think the minimal amount of duplicated code to achieve this would be constructing the correct PropertyConfig
instance in a global location and then referencing it from all the PB tests in order to set it, which (when using C#) would be achieved via this method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could also make an extension method of Property
that configures them to all be the same and call that on each property you want it to apply to.
public static Property ConfigureSpecifically(this Property property) =>
property.WithTests(1000).WithoutShrinkLimit().Whatever();
// in a test
var someProperty = // ...
someProperty.ConfigureSpecifically().OverrideLocally().Check();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point.
I think I would still keep this myself, but I won't argue further in favor of keeping it.
@@ -71,91 +71,46 @@ type PropertyExtensions private () = | |||
let (Property property) = property | |||
Property.report property | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should only expose a single way to configure properties; this new API should be the only API.
Gonna let #353 go first as it's smaller, more mature, and may possibly establish designs/patterns that this will follow. |
Closes #343
Okay, I got sucked into this.
Marking as a draft cause I still need to
Buuuuut for the most part I think it's mostly done.