File Coverage

blib/lib/Test/QuickGen.pm
Criterion Covered Total %
statement 52 53 98.1
branch 13 18 72.2
condition 5 12 41.6
subroutine 14 14 100.0
pod 9 9 100.0
total 93 106 87.7


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