Skip to content

Hedgehog with batteries included: Auto-generators, extra combinators, and more.

License

Notifications You must be signed in to change notification settings

hedgehogqa/fsharp-hedgehog-experimental

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fsharp-hedgehog-experimental

NuGet AppVeyor

Hedgehog with batteries included: Auto-generators, extra combinators, and more.

Features

  • Auto-generation of arbitrary types
  • Generation of functions
  • Lots of convenient combinators

Examples

Convenience combinators

Generate lists without having to explicitly use Range:

let! exponentialList = Gen.bool |> GenX.eList 1 5 // Same as Gen.list (Range.exponential 1 5)
let! linearList      = Gen.bool |> GenX.lList 1 5 // Same as Gen.list (Range.linear 1 5)
let! constantList    = Gen.bool |> GenX.cList 1 5 // Same as Gen.list (Range.constant 1 5)

Generate strings without having to explicitly use Range:

let! exponentialStr = Gen.alpha |> GenX.eString 1 5 // Same as Gen.string (Range.exponential 1 5)
let! linearStr      = Gen.alpha |> GenX.lString 1 5 // Same as Gen.string (Range.linear 1 5)
let! constantStr    = Gen.alpha |> GenX.cString 1 5 // Same as Gen.string (Range.constant 1 5)

Generate a random shuffle/permutation of a list:

let lst = [1; 2; 3; 4; 5]
let! shuffled = GenX.shuffle lst // e.g. [2; 1; 5; 3; 4]

(Note that the shuffle may produce an identical list; this is more likely for shorter lists.)

Shuffle/permute the case of a string:

let str = "abcde"
let! shuffled = GenX.shuffleCase str // e.g. "aBCdE"

Generate an element that is not equal to another element (direct or option-wrapped):

// Generates an int that is not 0
let! notZero = Gen.int (Range.exponentialBounded()) |> GenX.notEqualTo 0

// Also generates an int that is not 0
let! notZero = Gen.int (Range.exponentialBounded()) |> GenX.notEqualToOpt (Some 0)

// Can generate any int
let! anyInt = Gen.int (Range.exponentialBounded()) |> GenX.notEqualToOpt None

Generate a string that does not equal another string, start with another string, or is a substring of another string:

let strGen = Gen.alpha |> GenX.lString 1 5

let! ex1 = strGen |> GenX.notEqualTo "A" // Does not generate "A" (same function as previous example)
let! ex2 = strGen |> GenX.iNotEqualTo "A" // Case insensitive, does not generate "A" or "a"
let! ex3 = strGen |> GenX.notSubstringOf "fooBar" // Does not generate e.g. "Bar" (but "bar" is OK)
let! ex4 = strGen |> GenX.iNotSubstringOf "fooBar" // Case insensitive, does not generate e.g. "Bar" or bar"
let! ex5 = strGen |> GenX.notStartsWith "foo" // Does not generate e.g. "foobar" (but "Foobar" is OK)
let! ex6 = strGen |> GenX.iNotStartsWith "foo" // Case insensitive, does not generate e.g. "foobar" or "Foobar"

Generate an item that is not in a specified list:

let! str = Gen.int (Range.exponentialBounded()) |> GenX.notIn [1; 2] // Does not generate 1 or 2

Generate a list that does not contain a specified item:

// Produces a list not containins 2, e.g. [1; 5; 7]. Note that this is a filter and not
// a removal after the list has been generated, so the length of the list is unaffected.
let! intList = Gen.int (Range.exponentialBounded()) |> GenX.cList 3 3 |> GenX.notContains 2

Generate a list that contains a specified item at a random index:

// Generates a list and inserts the element. The list is 1 element longer than it would otherwise have been.
let! intList = Gen.int (Range.exponentialBounded()) |> GenX.cList 3 3 |> GenX.addElement 2

Generate null some of the time, or don't generate nulls:

let strGen = Gen.alpha |> GenX.eString 1 5

// Generates null part of the time (same frequency as Gen.option generates None)
let nullStrGen = strGen |> GenX.withNull

// Does not generate null
let noNullStrGen = nullStrGen |> GenX.noNull

Generate sorted/distinct tuples (2, 3 or 4 elements):

// Can produce 'a', 'a', 'c' but not 'a', 'c', 'b'
let! x, y, z = Gen.alpha |> Gen.tuple3 |> GenX.sorted3

// Can produce 'b', 'a', 'c' but not 'b', 'b', 'c'
let! x, y, z = Gen.alpha |> Gen.tuple3 |> GenX.distinct3

// Strictly increasing - can produce 'a', 'b', 'c' but not 'a', 'a', 'c'
let! x, y, z = Gen.alpha |> Gen.tuple3 |> GenX.increasing3

Generate a date range:

// Generates two dates at least 1 and at most 10 days apart, each with random time of day.
// The interval increases linearly with the implicit size parameter.
let! d1, d2 = GenX.dateInterval (Range.linear 1 10)

Generate a function:

// Generates a list using inpGen together with a function that maps each of the distinct
// elements in the list to values generated by outGen. Distinct elements in the input list
// may map to the same output values. For example, [2; 3; 2] may map to ['A'; 'B'; 'A'] or
// ['A'; 'A'; 'A'], but never ['A'; 'B'; 'C']. The generated function throws if called with
// values not present in the input list.
let intGen = Gen.int (Range.exponentialBounded())
let charListGen = Gen.alpha |> GenX.eList 1 10
let! chars, f = charListGen |> GenX.withMapTo intGen
// chars : char list
// f : char -> int


// Generates a list using inpGen together with a function that maps each of the
// distinct elements in the list to values generated by outGen. Distinct elements
// in the input list are guaranteed to map to distinct output values. For example,
// [2; 3; 2] may map to ['A'; 'B'; 'A'], but never ['A'; 'A'; 'A'] or ['A'; 'B'; 'C'].
// Only use this if the output space is large enough that the required number of distinct
// output values are likely to be generated. The generated function throws if called with
// values not present in the input list.
let intGen = Gen.int (Range.exponentialBounded())
let charListGen = Gen.alpha |> GenX.eList 1 10
let! chars, f = charListGen |> GenX.withDistinctMapTo intGen
// chars : char list
// f : char -> int

Auto-generation

Generate any type automatically using default auto-generators for primitive types:

// Can generate all F# types (unions, records, lists, etc.) as well as POCOs
// with mutable properties or constructors.

type Union =
  | Husband of int
  | Wife of string
  
type Record =
  {Sport: string
   Time: TimeSpan}
   
// Explicit type parameter may not be necessary if it can be inferred.
let! union = GenX.auto<Union>
let! record = GenX.auto<Record>

// Recursive types are supported. By default, recurses at most once (subject to change).
type Recursive =
  {OptChild: Recursive option
   LstChild: Recursive list}
let! recursive = GenX.auto<Recursive>
// E.g. {OptChild = Some {OptChild = None; LstChild = []}; LstChild = []}
// Note that you may need to adjust the defaults when any kind of sequence is involved
// (see below), since by default the range for generated sequences are Range.exponential 0 50
// (subject to change).

Generate any type automatically and override default generators and settings:

let! myVal =
  {GenX.defaults with 
     SeqRange = Range.exponential 1 10
     RecursionDepth = 2}
  // Will use this generator for all ints
  |> AutoGenConfig.addGenerator (Gen.int (Range.linear 0 10))
  // Will use this generator when generating its return type
  |> AutoGenConfig.addGenerator Gen.myCustomGen
  // Generate using the config above
  |> GenX.autoWith<MyType>

If you’re not happy with the auto-gen defaults, you can of course create your own generator that calls GenX.autoWith with your chosen config and use that everywhere.

Deployment checklist

For maintainers.

  • Make necessary changes to the code
  • Update the changelog
  • Update the version and release notes in the fsproj file (incrementing the version on master is what triggers the deployment to NuGet)
  • Commit and push

Each commit to master