File Coverage

blib/lib/Test/QuickGen.pm
Criterion Covered Total %
statement 57 58 98.2
branch 16 20 80.0
condition 10 17 58.8
subroutine 15 15 100.0
pod 9 9 100.0
total 107 119 89.9


line stmt bran cond sub pod time code
1             package Test::QuickGen;
2              
3 3     3   425977 use v5.16;
  3         13  
4 3     3   18 use strict;
  3         16  
  3         101  
5 3     3   14 use warnings;
  3         7  
  3         225  
6 3     3   17 use Carp qw(croak);
  3         20  
  3         235  
7 3     3   19 use Scalar::Util qw(looks_like_number);
  3         6  
  3         168  
8 3     3   25 use Exporter 'import';
  3         7  
  3         3018  
9              
10             our $VERSION = '0.1.1';
11              
12             our @EXPORT_OK = qw(
13             ascii_string between id string_of pick nullable words
14             utf8_string utf8_sanitized
15             );
16             our %EXPORT_TAGS = (
17             all => \@EXPORT_OK,
18             utf8 => [qw(utf8_string utf8_sanitized)],
19             basic => [qw(id between pick nullable)],
20             );
21              
22             =head1 NAME
23              
24             Test::QuickGen - Utilities for generating random test data
25              
26             =head1 SYNOPSIS
27              
28             use Test::QuickGen qw(:all);
29              
30             my $id = id();
31             my $str = ascii_string(10);
32             my $utf8 = utf8_string(20);
33             my $clean = utf8_sanitized(15);
34              
35             my $rand = between(1, 100);
36             my $opt = nullable("value");
37             my $item = pick(qw(a b c));
38              
39             my $words = words(\&ascii_string, 5);
40              
41             =head1 DESCRIPTION
42              
43             C provides a set of utility functions for generating random
44             data, primarily intended for testing purposes. These generators are simple,
45             fast, and have minimal dependencies.
46              
47             All functions are exported by default.
48              
49             =head1 COMMAND LINE TOOL
50              
51             This module comes bundled with an optional test runner, see L for
52             more details.
53              
54             =head1 IMPORTING
55              
56             Nothing is exported by default.
57              
58             Import functions explicitly:
59              
60             use Test::QuickGen qw(id ascii_string);
61              
62             Import groups of functions using tags:
63              
64             use Test::QuickGen qw(:all);
65             use Test::QuickGen qw(:utf8);
66             use Test::QuickGen qw(:basic);
67              
68             =over 4
69              
70             =item * C<:all>
71              
72             All available functions.
73              
74             =item * C<:utf8>
75              
76             UTF-8 specific functions.
77              
78             =item * C<:basic>
79              
80             Simple utils like C or C.
81              
82             =back
83              
84             See source for exact composition of the imports.
85              
86             =head1 FUNCTIONS
87              
88             =head2 id
89              
90             my $id1 = id();
91             my $id2 = id();
92              
93             # $id1 != $id2
94              
95             Returns a monotonically increasing integer starting from 0.
96              
97             The counter is process-local and resets each time the program runs.
98              
99             =cut
100             sub id {
101 6     6 1 222351 state $id = 0;
102 6         18 $id++;
103             }
104              
105             =head2 string_of($n, @chars)
106              
107             my $str = string_of(10, qw(a b c));
108              
109             Generates a random string of length C<$n> using the provided list of characters C<@chars>.
110              
111             =over 4
112              
113             =item *
114              
115             C<$n> must be a non-negative integer.
116              
117             =item *
118              
119             At least one character must be provided.
120              
121             =item *
122              
123             Characters are selected uniformly at random.
124              
125             =back
126              
127             =cut
128             sub string_of {
129 12     12 1 8083 my ($n, @chars) = @_;
130              
131 12 100       247 croak 'string_of: empty character set' unless @chars;
132            
133 11         23 my $str = '';
134 11         34 for (1..$n) {
135 275         613 $str .= $chars[rand @chars];
136             }
137 11         110 $str;
138             }
139              
140             =head2 ascii_string($n)
141              
142             my $str = ascii_string(10);
143              
144             Generates a random ASCII string length C<$n>.
145              
146             The character set includes all lowercase letters (a-z), uppercase letters (A-Z),
147             digits (0-9) and underscore (_).
148              
149             =cut
150             sub ascii_string {
151 7     7 1 12836 my ($n) = @_;
152             # TODO: include other ASCII characters too
153 7         111 string_of($n, 'a'..'z', 'A'..'Z', '0'..'9', '_');
154             }
155              
156             =head2 utf8_string($n)
157              
158             my $str = utf8_string(10);
159              
160             Generates a random UTF-8 string of C<$n> characters.
161              
162             The generator:
163              
164             =over 4
165              
166             =item *
167              
168             Includes visible Unicode characters up to code point C<0x2FFF>.
169              
170             =item *
171              
172             Excludes control characters and invalid Unicode ranges.
173              
174             =item *
175              
176             Skips surrogate pairs and non-characters.
177              
178             =back
179              
180             Note: Because characters may vary in byte length, this function targets
181             character count (not byte length).
182              
183             =cut
184             sub utf8_string {
185 14     14 1 94109 my ($n) = @_;
186 14         38 my $str = '';
187 14         76 while (length($str) < $n) {
188             # skip non-visible ASCII characters (0x00-0x19)
189             # include everything up to 0x2FFF (extended UTF-8)
190 201         427 my $code_point = between(0x20, 0x2FFF);
191              
192             # skip problematic unicode points
193 201 50 33     507 next if ($code_point >= 0xD800 && $code_point <= 0xDFFF); # surrogate pairs
194 201 50 33     457 next if ($code_point >= 0xFDD0 && $code_point <= 0xFDEF); # non characters
195             # also non characters
196 201 50 33     722 next if ($code_point % 0x10000 == 0xFFFE || $code_point % 0x10000 == 0xFFFF);
197 201 100 100     674 next if ($code_point >= 0x7F && $code_point <= 0x9F); # control characters
198              
199 200         574 $str .= chr($code_point);
200             }
201 14         93 $str;
202             }
203              
204             =head2 utf8_sanitized($n)
205              
206             my $clean = utf8_sanitized(10);
207              
208             Generates a UTF-8 string of length C<$n> and removes all non-alphanumeric
209             characters, retaining only:
210              
211             =over 4
212              
213             =item *
214              
215             Unicode letters (C<\p{L}>)
216              
217             =item *
218              
219             Unicode numbers (C<\p{N}>)
220              
221             =item *
222              
223             Whitespace
224              
225             =back
226              
227             If all characters are filtered out, the function retries until a non-empty
228             string is produced.
229              
230             =cut
231             sub utf8_sanitized {
232 7     7 1 43610 my ($n) = @_;
233 7         18 my $s = utf8_string($n);
234             # exit early before stripping if the intended result is an empty string
235 7 100       44 return $s if $s eq '';
236              
237 5         99 $s =~ s/[^\p{L}\p{N}\s]//gu;
238              
239             # sometimes all characters get filtered, try again and hope for the best
240 5 50       19 if ($s eq '') {
241 0         0 return utf8_sanitized($n);
242             }
243              
244 5         19 $s;
245             }
246              
247             =head2 words($gen, $n, $max_len = 70)
248              
249             my $str = words(\&string_generator, 5);
250              
251             Generates a string made up of C<$n> space-separated "words".
252              
253             Each word is produced by calling the generator function C<$gen>.
254              
255             =over 4
256              
257             =item * C<$gen>
258              
259             A coderef that is called once per word.
260              
261             It accepts a single integer argument (the desired length), and returns a string.
262              
263             For example:
264              
265             sub string_generator {
266             my ($len) = @_;
267             # return a string of length $len
268             }
269              
270             =item * C<$max_len>
271              
272             An optional parameter to set the maximum length (inclusive) of a word.
273             Defaults to 70. Must be a positive number.
274              
275             =item * Word generation
276              
277             For each of the C<$n> words, a random length between 1 and C<$max_len> is
278             chosen. That length is passed to C<$gen>, which returns the word.
279              
280             =item * Output format
281              
282             The generated words are joined together with a single space.
283              
284             =back
285              
286             Example:
287              
288             words(\&ascii_string, 3);
289             # might return: "aZ3 kLm92 Q"
290              
291             =cut
292             sub words {
293 4     4 1 6226 my ($gen, $n, $max_len) = @_;
294              
295 4   100     21 $max_len //= 70;
296 4 100 66     223 croak '$max_len must be a positive number'
297             unless looks_like_number($max_len) && $max_len > 0;
298              
299 3         11 my @words = map { $gen->(between(1, $max_len)) } (1..$n);
  7         31  
300 3         29 join ' ', @words;
301             }
302              
303             =head2 between($min, $max)
304              
305             my $n = between(1, 10);
306              
307             Returns a random integer between C<$min> and C<$max> (inclusive).
308              
309             The distribution is uniform and C<$min> must be <= C<$max>.
310              
311             =cut
312             sub between {
313 225     225 1 13195 my ($min, $max) = @_;
314 225 100       699 croak "between: max must be larger or equal to min" if $max < $min;
315 224         524 $min + int(rand($max - $min + 1));
316             }
317              
318             =head2 nullable($val)
319              
320             my $value = nullable("data");
321              
322             Returns either the given value or C.
323              
324             25% chance of returning C, 75% chance of returning the original value.
325             Useful for testing optional fields.
326              
327             =cut
328             sub nullable {
329 16     16 1 5965 my ($val) = @_;
330 16 100       36 if (rand() < 0.25) {
331 4         7 undef;
332             } else {
333 12         53 $val;
334             }
335             }
336              
337             =head2 pick(@items)
338              
339             my $item = pick(qw(a b c));
340              
341             Returns a random element from the provided list.
342              
343             If provided an empty list, will return C. Randomness is uniform in
344             its distribution.
345              
346             =cut
347 22     22 1 12386 sub pick { $_[rand @_] }
348              
349             =head1 NOTES
350              
351             =over 4
352              
353             =item *
354              
355             These functions are not cryptographically secure.
356              
357             =item *
358              
359             They are intended for testing, fuzzing, and data generation only.
360              
361             =back
362              
363             =head1 AUTHOR
364              
365             Antonis Kalou Ekalouantonis@protonmail.comE
366              
367             =head1 LICENSE
368              
369             This library is free software; you can redistribute it and/or modify it under
370             the same terms as Perl itself. See F for details.
371              
372             =cut
373              
374             1;