Test::...nerator(3pm)

πŸ“¦
version 0.5001 Β· 4 versions
, in debian bookworm, debian trixie, ubuntu noble, ubuntu resolute, ubuntu stonking

πŸ’– Test::LectroTest::Generator β€” the manual, but make it cute πŸ’–

okay so bestie, welcome to the docs for Test::LectroTest::Generator β€” a User Contributed Perl Documentation page, and yes it's the whole thing, no skimming, we're doing this together 🫢


✨ NAME ✨

Test::LectroTest::Generator β€” Random value generators and combinators 🌈


🌸 VERSION 🌸

version 0.5001 β€” yes, the version. She's vintage. She's iconic. πŸ’…


πŸ¦‹ SYNOPSIS πŸ¦‹

okay so here's the whole vibe in code, look at her:

use Test::LectroTest::Generator qw(:common :combinators);

my $int_gen = Int;
my $pct_gen = Int( range=>[0,100] );
my $flt_gen = Float( range=>[0,1] );
my $bln_gen = Bool;
my $chr_gen = Char( charset=>"a-z" );
my $str_gen = String( charset=>"A-Z0-9", length=>[3,] );
my $ary_gen = List( Int(sized=>0) );
my $hsh_gen = Hash( $str_gen, $pct_gen );
my $uni_gen = Unit( "e" );  # always returns "e"
my $elm_gen = Elements("e1", "e2", "e3", "e4");

for my $sizing_guidance (1..100) {
    my $i = $int_gen->generate( $sizing_guidance );
    print "$i ";
}
print "\n";

# generates single digits
my $digit_gen  = Elements( 0..9 );  # or Int(range=>[0,9],sized=>0)

# generates SSNs like "910-77-2236"
my $ssn_gen    = Paste( Paste( ($digit_gen) x 3 ),
                        Paste( ($digit_gen) x 2 ),
                        Paste( ($digit_gen) x 4 ),
                        glue => "-"                );

# print 10 SSNs
print( map {$ssn_gen->generate($_)."\n"} 1..10 );

my $english_dist_vowel_gen =
    Frequency( [8.167,Unit("a")], [12.702,Unit("e")],
               [6.996,Unit("i")], [ 7.507,Unit("o")],
               [2.758,Unit("u")] );
    # Source: http://www.csm.astate.edu/~rossa/datasec/frequency.html

πŸ‘‘ DESCRIPTION πŸ‘‘

So. This module hands you random value generators for all the common data types, plus an interface and a whole toolkit so you can build your own generators. It also gives you generator combinators β€” which is just a fancy way of saying you can smash simple generators together to make more complex ones. Slay. πŸ”₯

A generator is an object with a method called generate. You give it one argument β€” the size β€” and it hands you back a brand-new random value. The generated value is always a scalar. And if a generator makes data structures? Then it returns references to them, not the raw thing. iykyk 🧚


πŸ’« Sizing guidance πŸ’«

The generate method treats its size argument as guidance about how complicated the value it makes should be. Typically: smaller size values β†’ smaller numbers, shorter strings, shorter lists. Some generators just don't care about sizing at all (it makes no sense for them), and the ones that do use it can be told to ignore it with the sized modifier. πŸ™ˆ

And why do we even do this? Because the whole point of sizing is to let LectroTest start out with simple values and then, as testing goes on, slowly ramp up the complexity. That way, when something obviously breaks, the counterexample you get is actually readable instead of a nightmare. πŸ’€


πŸŽ€ Generators πŸŽ€

These functions below create fully-formed generators, ready to go, no assembly required. They get exported into your code's namespace if you ask for :generators or :all when you use this module.

Every generator has a generate method you can call to pull a new random value out of it. Obsessed. πŸ’–


⭐ Int

my $gen = Int( range=>[0,9], sized=>0 );

Creates a generator for integer values β€” by default in the range [-32768,32767], inclusive. But you can change that with the optional range modifier.

  • Int( range=>[low, high] ) β€” constrains the generated values to the range [low, high], inclusive. By default the range is [-32768, 32767].
Note: if your range is empty (i.e. low > high), LectroTest will complain. 😭
Note: if zero is not inside the range you gave, then sizing makes no sense β€” because the intersection of your range and the sizing range can be empty. So you must turn off sizing with sized=>0. If you forget? LectroTest complains. Again. πŸ™ˆ
  • Int( sized=>bool ) β€” if true (that's the default), it constrains the absolute value of the generated integers to the sizing guidance handed to the generate method. If not, the generated values are constrained only by the range.

⭐ Float

my $gen = Float( range=>[-2.0,2.0], sized=>1 );

Creates a generator for floating-point values β€” by default in the range [-32768.0,32768.0), but you can change that with the optional range modifier. And fyi, by default Float generators are sized. ✨

  • Float( range=>[low, high] ) β€” constrains the generated values to the range [low, high). By default, that's [-32768.0,32768.0). (Heads up: the high value itself can never actually be generated β€” but values infinitesimally close to it absolutely can. sneaky πŸ’…)
Note: if your range is empty (i.e. low > high), LectroTest will complain. 😭
Note: if zero isn't in the range you provide, sizing makes no sense β€” the intersection of your range and the sizing range can be empty β€” so you gotta turn off sizing with sized=>0. Forget, and LectroTest complains. 🫠
  • Float( sized=>bool ) β€” if true (the default), it constrains the absolute value of the generated values to the sizing guidance given to generate. Otherwise, values are constrained only by the range.

⭐ Bool

my $gen = Bool;

Creates a generator for boolean values: 0 for false, 1 for true. This one ignores sizing guidance entirely. She does not care. πŸ‘‘


⭐ Char

my $gen = Char( charset=>"A-Za-z0-9_" );

Creates a generator for characters. By default the characters come from the ASCII range [0,127], inclusive β€” but you can change that with the charset modifier:

  • Char( charset=>cset ) β€” characters get drawn from the character set given by the character-set specification cset. The syntax of cset is similar to the Perl tr built-in: it's a string made of characters and character ranges.
    • c β€” adds the character c to the set.
    • c-d β€” adds the characters in the range c through d, inclusive, to the set.
Note: if c is lexicographically greater than d, the range is empty, and no characters will be added to the set. πŸ’€

Examples, bestie:

  • charset=>"abcdwxyz" β€” the characters "a", "b", "c", "d", "w", "x", "y", and "z" are in the set.
  • charset=>"a-dx-z" β€” the shorter version of the previous example.
  • charset=>"\x00-\x7f" β€” the ASCII character set.
  • charset=>"-_A-Za-z0-9" β€” the set contains "-", "_", upper- and lower-case ASCII letters, and the digits 0-9. Notice that the dash must come first, so it doesn't get misread as denoting a range of characters. 🎯

⭐ List(elemgen)

my $gen = List( Bool, length=>[1,10] );

Creates a generator for lists (returned as array refs, obviously). The elements get generated by the generator you pass as elemgen. The lengths of the generated lists are constrained by sizing guidance at the time of generation. And you can override the default sizing behaviour with the optional length modifier. πŸ¦‹

When the list generator calls the element generator, it divides the sizing guidance by the length of the list. For example, if the list being generated will have 7 elements, then when the list generator calls the element generator for each element, it scales the sizing guidance by 1/7. That way the sizing guidance puts a rough constraint on the total number of elements produced, no matter how deep the list structure goes. 🧚

  • List( ..., length=>N ) β€” generated lists are exactly length N.
  • List( ..., length=>[M,] ) β€” generated lists are at least length M. (Max length is constrained by the sizing factor.)
  • List( ..., length=>[M,N] ) β€” generated lists have length between M and N, inclusive. Sizing guidance is ignored.
Advanced Note: if you give more than one elemgen, they get used in turn to create successive elements. In that case, the length of the list gets multiplied by the number of generators you gave. For example, giving two generators creates double-length lists. 😳

⭐ Hash(keygen, valgen)

my $gen = Hash( String( charset=>"A-Z", length=>3 ),
                Float( range=>[0.0, 100.0] );

Creates a generator for hashes (returned as hash refs). The keys come from the generator you pass as keygen, and the values come from valgen.

The Hash generator also takes an optional length modifier specifying the desired hash length (= number of keys):

  • Hash( ..., length=>length-spec ) β€” specifies the desired length of the generated hashes, using the same length-spec syntax as the List generator.
Note: the generated hashes may end up smaller than expected, because of key collision. it's giving chaos πŸŒͺ️

⭐ String

my $gen = String( length=>[3,], charset=>"A-Z" );

Creates a generator for strings. By default the strings get drawn from the ASCII character set (0 through 127) and have a length constrained by the sizing factor. Both of those defaults can be changed with modifiers:

  • String( charset=>cset ) β€” characters get drawn from the character set given by the character-set specification cset. The syntax of cset is similar to the Perl tr operator: a string made of characters and character ranges. See Char above for the full description. πŸ’–
  • String( length=>length-spec ) β€” specifies the desired length of the generated strings, using the same length-spec syntax as the List generator.

⭐ Elements(e1, e2, ...)

my $gen = Elements( "alpha", "beta", "gamma" );

Creates a generator that picks among the given elements e1, e2, ... with equal probability. Every call to generate returns one of the element values. Sizing guidance has no effect on this generator.

Note: this generator builder does not accept modifiers. If you pass any, they'll get interpreted as elements to add to the pool the generator randomly picks from β€” which is probably not what you want. πŸ™ˆ

⭐ Unit(e)

my $gen = Unit( "alpha" );

Creates a generator that always returns the value e. Not super useful on her own, but she's a great building block for combinators to chew on. And naturally, sizing guidance has no effect on this generator either.

Note: this generator builder does not accept modifiers. periodt. πŸ’…

🧚 Generator combinators ✨

okay bestie, these are the glow-up tools. You take your little baby generators and you combine them into fancier ones, it's giving build-a-bear but for randomness πŸ’…

Quick heads-up before we dive in: these combinators only get exported into your code's namespace if you ask for :combinators or :all when you use this module. So if you're sitting there like "why is Paste undefined bestie" β€” that's why. Mystery solved πŸ•΅οΈβ€β™€οΈ


πŸŽ€ Paste(gens..., glue=>str)

my $gen = Paste( (String(charset=>"0-9",length=>4)) x 4,
                 glue => " " );
# gens credit-card numbers like "4592 9459 9023 1369"

my $lgen = Paste( List( String(charset=>"0-9",length=>4)
                      , length=>4 ), glue => " " );
# another way of doing the same

This one makes a combined generator that generates values by joining together the values that each of the supplied sub-generators gens spits out. (And if a generated value is a list, its elements get "flattened" into the rest of the generated results before the joining happens.) Then the resulting string is what you get back. Periodt.

The joining is done with whatever glue string str you hand over. And if you don't provide a glue modifier at all? Default glue is the empty string, so everything just smooshes together 🌸

The sizing guidance you give to the combined generator gets passed unchanged to every single one of the sub-generators. No edits, no notes, straight through.


πŸŽ€ OneOf(gens...)

my $gen = OneOf( Unit(0), List(Int,length=>3) );
# generates scalar 0 or a 3-element list of integers

Combined generator that generates each value by picking one of the sub-generators in gens at random (equal probability for each, no favourites here πŸ‘‘) and then using that lucky winner to generate the output value.

The sizing guidance given to the combined generator gets passed unchanged to whichever sub-generator got picked.

Note: This combinator does not accept modifiers. Like, at all. Don't try it πŸ™ˆ

πŸŽ€ Frequency([freq1, gen1], [freq2, gen2], ...)

my $gen = Frequency( [50, Unit("common"     )],
                     [35, Unit("less common")],
                     [15, Unit("uncommon"   )] );
# generates one of "common", "less common", or
# "uncommon" with respective probabilities
# 50%, 35%, and 15%.

Combined generator that generates each value by picking one of gen1 or gen2 or ... at random and then using it to generate the output value. Each generator gets selected with a probability proportional to its associated frequency. (And lowkey, if every frequency you give is the same, Frequency just becomes OneOf β€” same vibe, different name.) The frequencies can be any non-negative numerical values you want, and internally they get normalized onto a 0-to-1 scale. One rule tho: at least one frequency must be greater than zero, or it's not gonna work, bestie.

The sizing guidance given to the combined generator gets passed unchanged to the selected sub-generator.

Note: This combinator does not accept modifiers.

πŸŽ€ Each(gens...)

my $gen = Each( Unit(1), Unit("X") );
# always generates [ 1, "X" ]

Makes a generator that returns a list (an array ref) whose successive elements are the successive values generated by the given generators gens. So it's like a little conga line of values 🌈

The sizing guidance given to the combined generator gets passed unchanged to each sub-generator.

Note: This combinator does not accept modifiers.

(Note for the technical buffs out there: Each(...) is exactly equivalent to List(..., length=>1).) iykyk πŸ€“


πŸŽ€ Apply(fn, gens...)

my $gen = Apply( sub { $_[0] x $_[1] }
               , Unit("X"), Unit(4) );
# always generates "XXXX"

Makes a generator that applies the given function fn to arguments generated from each of the given sub-generators gens, then returns the resulting value. Every sub-generator contributes exactly one value, and those values get passed to fn as arguments in the same order the sub-generators were given to Apply. Order matters, bestie, don't shuffle πŸ’–

The sizing guidance given to the combined generator gets passed unchanged to each sub-generator.

Note: The function fn is always evaluated in scalar context. If you need to generate an array, return it as an array reference. No exceptions.
Note: This combinator does not accept modifiers.

πŸŽ€ Map(fn, gens...)

my $gen = Map( sub { "X" x $_[0] }
             , Unit(4), Unit(3), Unit(0) );
# always generates [ "XXXX", "XXX", "" ]

Makes a generator that applies the given function fn to the values generated by the given generators gen, one at a time, and returns a list (an array ref) whose elements are each of the successive results. It's giving factory line energy 🏭✨

The sizing guidance given to the combined generator gets passed unchanged to each sub-generator.

Note: The function fn is always evaluated in scalar context. If you need to generate an array, return it as an array reference.
Note: This combinator does not accept modifiers.

πŸŽ€ Concat(gens...)

my $gen = Concat( List( Unit(1),   length=>3 )
                , List( Unit("x"), length=>1 ) );
# always generates [1, 1, 1, "x"]

Makes a generator that concatenates the values generated by each of its sub-generators, resulting in a list (returned as an array reference). The values coming back from the sub-generators are expected to be lists (array refs). But if a sub-generator hands you back a scalar value instead, it just gets treated like a single-element list containing that value. Flexible, iconic 🫢

The sizing guidance given to the combined generator gets passed unchanged to each sub-generator.

Note: If a sub-generator returns something that is neither a list nor a scalar, you will get a run-time error. 😭
Note: This combinator does not accept modifiers.

πŸŽ€ Flatten(gens...)

my $gen = Flatten( Unit( [[[[[[ 1 ]]]]]] ) );
# generates [1]

Flatten is literally just like Concat, except it recursively flattens any sublists generated by the generators gen and then concatenates them, producing a final list of depth one β€” no matter how deeply nested those sublists got. It's giving "un-nesting your entire life" πŸ¦‹

The sizing guidance given to the combined generator gets passed unchanged to each sub-generator.

Note: If a sub-generator returns something that is neither a list nor a scalar, you will get a run-time error.
Note: This combinator does not accept modifiers.

πŸŽ€ ConcatMap(fn, gens)

sub take_odds { my $x = shift;
                $x % 2 ? [$x] : [] }
my $gen = ConcatMap( \&take_odds
                   , Unit(1), Unit(2), Unit(3) );
# generates [1, 3]

Makes a generator that applies the function fn to each of the values generated by the given generators gen in turn, and then concatenates the results. Filter-and-merge, we love to see it πŸ’…

The sizing guidance given to the combined generator gets passed unchanged to each sub-generator.

Note: The function fn is always evaluated in scalar context. If you need to generate an array, return it as an array reference.
Note: If a sub-generator returns something that is neither a list nor a scalar, you will get a run-time error.
Note: This combinator does not accept modifiers.

πŸŽ€ FlattenMap(fn, gens)

my $gen = FlattenMap( sub { [ ($_[0]) x 3 ] }
                    , Unit([1]), Unit([[2]]) );
# generates [1, 1, 1, 2, 2, 2]

Makes a generator that applies the function fn to each of the values generated by the given generators gen in turn, and then flattens and concatenates the results. Deep-clean energy ✨

The sizing guidance given to the combined generator gets passed unchanged to each sub-generator.

Note: The function fn is always evaluated in scalar context. If you need to generate an array, return it as an array reference.
Note: If a sub-generator returns something that is neither a list nor a scalar, you will get a run-time error.
Note: This combinator does not accept modifiers.

πŸŽ€ Sized(fn, gen)

my $gen = Sized { 2 * $_[0] } List(Int);
    # ^ magnify sizing guidance by factor of two
my $gen2 = Sized { 10 } Int;
    # ^ use constant guidance of 10

Makes a generator that adjusts sizing guidance by running it through the function fn. Then it calls the generator gen with that adjusted guidance and returns the result. So you're basically remixing the size dial πŸŽ›οΈ

Note: This combinator does not accept modifiers.

πŸ¦‹ Rolling your own generators πŸ¦‹

Okay bestie, plot twist: you can absolutely make your own generators. Just create any object that has a generate method. That method should take sizing guidance size as its first argument and β€” if it makes sense for what you're doing β€” adjust the complexity of the values it generates accordingly. Custom generator, main character energy πŸ‘‘

The easiest way to make a generator is by using the magic function Gen. It promotes a block of code into a generator. For example, here's a home-brew generator for times in ctime(3) format, built on top of an Int generator:

use Test::LectroTest::Generator qw( :common Gen );

my $time_gen = Int(range=>[0, 2_147_483_647], sized=>0);
my $ctime_gen = Gen {
    scalar localtime $time_gen->generate( @_ );
};

print($ctime_gen->generate($_), "\n") for 1..5;
# Fri Jun  2 18:13:21 1978
# Thu Mar 28 00:55:51 1974
# Wed Mar 26 06:41:09 2025
# Sun Sep 11 15:39:44 2016
# Fri Dec 26 00:39:31 1975

Alternatively, we could build the generator using the Apply combinator instead:

my $ctime_gen2 = Apply { localtime $_[0] } $time_gen;
Note: Gen is not exported into your code's namespace by default. If you wanna use it, you gotta import it by name β€” or import :all when you use this module. Don't forget, fr fr 🫢

🌸 EXAMPLES 🌸

okay bestie, here's where it gets fun. grab a snack, we're gonna look at some generators doing their thing. πŸ’–

✨ Simple examples ✨

use strict;
use Test::LectroTest::Generator qw(:common);

show("Ints (sized by default)", Int);

show("Floats (sized by default)", Float);

show("Percentages (unsized)",
     Int( range=>[0,100], sized=>0 ));

show("Lists (sized by default) of Ints (unsized) in [0,10]",
     List( Int( sized=>0, range=>[0,10] ) ));

show("Uppercase-alpha identifiers at least 3 chars long",
     String( length=>[3,], charset=>"A-Z" ));

show("Hashes (sized by default) of form AAA=>Digit",
     Hash( String( length=>3, charset=>"A-Z" ),
           Int( sized=>0, range=>[0,9] ) ));

sub show {
    print "\n", shift(), "\n";
    my ($gen) = @_;
    for (1..10) {
        my $val = $gen->generate($_);
        printf "Size %2d:  ", $_;
        if (ref $val eq "HASH") {
            my @pairs = map {"$_=>$val->{$_}"} keys %$val;
            print "{ @pairs }";
        }
        elsif (ref $val eq "ARRAY") {
            print "[ @$val ]"
        }
        else {
            print $val;
        }
        print "\n";
    }
}

like, that's a whole buffet of generators and a lil show helper that prints out ten samples at sizes 1 through 10. so satisfying to watch scroll by. πŸ“

πŸ’… Advanced examples πŸ’…

for these ones we're bringing in Data::Dumper to peek at the data structures we generate. also β€” and this is the glow-up β€” we import not only the common generator constructors (like Int) but also the generic Gen constructor, which lets us build generators out of blocks on the fly. ✨

use Data::Dumper;
use Test::LectroTest::Generator qw(:common Gen);

first up, a recipe for building a list of lists of integers:

my $loloi_gen = List( List( Int(sized=>0) ) );
print Dumper($loloi_gen->generate(10));

ngl you may wanna run that example several times to get a feel for the distribution of the generated output. the vibe shifts, fr fr. πŸ‘€

now, a more complicated example. here we build sized trees of random depth using a recursive set of generators. recursive generators, bestie. we're so back. 🌈

my $tree_gen = do {
    my $density = 0.5;
    my $leaf_gen = Int( sized=>0 );
    my $tree_helper = \1;
    my $branch_gen = List( Gen { $$tree_helper->generate(@_) } );
    $tree_helper = \Gen {
        my ($size) = @_;
        return rand($size) < $density
            ? $leaf_gen->generate($size)
            : $branch_gen->generate($size + 1);
    };
    $$tree_helper;
};

print Dumper($tree_gen->generate(30));

so the tea here: we define a tree as either a leaf or a branch, and we randomly decide between the two at each node in the growing tree. 🌳 leaves are just integers and become more likely when the sizing guidance diminishes (which happens as we go deeper β€” you feel that?). the code uses $density as a control knob for leaf density. (try re-running the above code after changing the value of $density. try 0, 1, and 2.) branches, on the other hand, are lists of trees. because branches generate trees, and trees generate branches, we use a reference trick to set up the mutually recursive relationship. this we encapsulate within a do block for tidiness. main character engineering honestly. πŸ‘‘


πŸ‘€ SEE ALSO πŸ‘€

Test::LectroTest gives a quick overview of automatic, specification-based testing with LectroTest.

πŸ’œ AUTHOR πŸ’œ

Tom Moertel (tom@moertel.com)

🌟 INSPIRATION 🌟

The LectroTest project was inspired by Haskell's QuickCheck module by Koen Claessen and John Hughes: http://www.cs.chalmers.se/~rjmh/QuickCheck/.

Copyright (c) 2004-13 by Thomas G Moertel. All rights reserved.

This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.


and that's a wrap, bestie!! 🫢 this whole page came to you via perl v5.34.0 on 2022-06-21, filed under Test::...nerator(3pm) β€” iconic, no notes. go generate something beautiful. πŸ’–βœ¨πŸ¦‹

πŸ“¦ where this page comes from