File Coverage

lib/Test/Tdd/Generator.pm
Criterion Covered Total %
statement 125 132 94.7
branch 19 24 79.1
condition 4 5 80.0
subroutine 24 24 100.0
pod 3 4 75.0
total 175 189 92.5


line stmt bran cond sub pod time code
1             package DB;
2              
3              
4             sub called_args {
5 13     13 0 49 my ($level) = @_;
6 13         286 my @foo = caller( $level + 2 );
7 13         130 return @DB::args;
8             }
9              
10             package Test::Tdd::Generator;
11              
12 2     2   3904 use strict;
  2         4  
  2         56  
13 2     2   10 use warnings;
  2         4  
  2         76  
14              
15 2     2   30 use File::Basename qw(dirname basename);
  2         6  
  2         286  
16 2     2   14 use File::Path qw(make_path);
  2         2  
  2         210  
17 2     2   1108 use File::Slurp qw(read_file write_file);
  2         71550  
  2         142  
18 2     2   1286 use Term::ANSIColor;
  2         16494  
  2         146  
19 2     2   1334 use Data::Dumper;
  2         13944  
  2         2650  
20              
21              
22             sub create_test {
23 13     13 1 66 my ($test_description, $opts) = @_;
24              
25 13         376 my ($package, $filename) = caller(0);
26 13         203 my ($_package, $_filename, $_line, $subroutine) = caller(1);
27 13         147 my ($test_path, $lib_path) = _find_test_and_lib_folders($filename);
28              
29 13         47 my $actual_test_path;
30 13 50       225 if (not -w $test_path) {
31 0         0 $actual_test_path = $test_path;
32 0         0 $test_path = "/tmp/t";
33             }
34              
35 13         80 my $test_file = $filename;
36 13         237 $test_file =~ s/$lib_path//;
37 13         212 $test_file =~ s/\.pm$/\.t/;
38 13         51 $test_file = $test_path . $test_file;
39              
40 13 50 66     341 if (-e $test_file && _test_exists($test_file, $test_description)) {
41 0         0 die "Test 'returns params plus foo' already exists on $test_file, please remove the create_test() line otherwise the test file would be recreated everytime you run the tests";
42             }
43              
44 13         3155 make_path dirname($test_file);
45              
46 13         213 my @args = DB::called_args(0);
47 13         51 my $globals = {};
48 13 100       172 $globals = _get_globals($opts->{globals}) if defined $opts->{globals};
49 13         110 my $input = { args => \@args, globals => $globals };
50 13         66 my $input_file = _save_input($test_file, $test_description, $input);
51              
52 13         97 my $global_expansion = "";
53 13 100       67 $global_expansion = "\n Test::Tdd::Generator::expand_globals(\$input->{globals});\n" if defined $opts->{globals};
54 13         90 my $test_body = <<"END_TXT";
55             it '$test_description' => sub {
56             my \$input = Test::Tdd::Generator::load_input(dirname(__FILE__) . "/input/$input_file");$global_expansion
57             my \$result = $subroutine(\@{\$input->{args}});
58              
59             is(\$result, "fixme");
60             };
61             END_TXT
62              
63 13         110 my $content = <<"END_TXT";
64             use strict;
65             use warnings;
66              
67             use Test::Spec;
68             use Test::Tdd::Generator;
69             use $package;
70             use File::Basename qw/dirname/;
71              
72             describe '$package' => sub {
73             $test_body
74             };
75              
76             runtests;
77             END_TXT
78              
79 13 100       273 if (-e $test_file) {
80 4         26 $content = read_file($test_file);
81 4         545 $content =~ s/(\};\n\nruntests)/$test_body$1/;
82             }
83              
84 13         129 write_file($test_file, $content);
85              
86 13         2489 print _get_instructions($test_file, $test_body, $test_path, $actual_test_path);
87             }
88              
89              
90             sub _get_instructions {
91 13     13   193 my ($test_file, $test_body, $test_path, $actual_test_path) = @_;
92              
93 13         219 my $run_instructions = color("green") . "Run it with:" . color("reset") . "\n\n provetdd $test_file\n\n";
94 13         778 my $move_instructions = "";
95 13 50       39 if ($actual_test_path) {
96 0         0 my $path_to_copy = dirname($actual_test_path);
97 0         0 $move_instructions = color("green") . "To copy it to the correct place run:" . color("reset") . "\n\n cp -R /tmp/t $path_to_copy\n\n";
98 0         0 $run_instructions =~ s/$test_path/$actual_test_path/;
99             }
100              
101 13         103 return color("green") . "Test created at $test_file:" . color("reset") . "\n\n$test_body\n" . $move_instructions . $run_instructions;
102             }
103              
104              
105             sub _find_test_and_lib_folders {
106 14     14   19123 my ($path) = @_;
107              
108 14         1326 my $dir = dirname($path);
109 14         105 my $previous = $dir;
110 14         113 while ($dir ne '.') {
111 42         120 my $test_folder = "$dir/t";
112 42 100       773 return ($test_folder, $previous) if -d $test_folder;
113 28         95 $previous = $dir;
114 28         745 $dir = dirname($dir);
115             }
116 0         0 die "Could not find t/ folder put the tests, searched in $path";
117             }
118              
119              
120             sub _save_input {
121 13     13   94 my ($test_file, $test_description, $input) = @_;
122              
123 13         467 my $inputs_folder = dirname($test_file) . '/input';
124 13         1608 make_path $inputs_folder;
125 13         181 $test_description =~ s/ /_/g;
126 13         674 my $test_file_base = basename($test_file, ".t");
127 13         58 my $input_file = "$test_file_base\_$test_description.dump";
128 13         34 my $input_file_path = "$inputs_folder/$input_file";
129              
130 13         81 local $Data::Dumper::Deparse = 1;
131 13         36 local $Data::Dumper::Maxrecurse = 0;
132 13         172 my $dumped = Dumper($input);
133 13         9706 $dumped =~ s/use strict/no strict/g;
134 13         145 write_file($input_file_path, $dumped);
135              
136 13         4302 return $input_file;
137             }
138              
139              
140             sub _test_exists {
141 4     4   25 my ($test_file, $test_description) = @_;
142              
143 4         60 my $content = read_file($test_file);
144 4         660 return $content =~ /it '$test_description'/;
145             }
146              
147              
148             sub _get_globals {
149 12     12   54338 my ($globals_names) = @_;
150              
151 12         59 return { map { $_ => _get_global_var($_) } @$globals_names };
  12         62  
152             }
153              
154              
155             sub _get_global_var {
156 25     25   59 my $name = shift;
157              
158 25         2321 my $global_var = eval "\$$name";
159 25 100       158 if ($global_var) {
160 13         94 return $global_var;
161             } else {
162 12         696 my %global_map = eval "\%$name";
163 12         110 %global_map = map { ($_ => _get_global_var($name . $_) ) } (keys %global_map);
  13         96  
164 12         211 return \%global_map;
165             }
166             }
167              
168              
169             sub expand_globals {
170 7     7 1 34198 my ($globals, $parent) = @_;
171 7   100     107 $parent ||= '';
172              
173 7         20 for my $key (keys %{$globals}) {
  7         61  
174 7         27 my $value = $globals->{$key};
175 7 100       86 if ($key =~ /::$/) {
176 4         67 expand_globals($value, $parent . $key);
177             } else {
178 3         536 eval("\$$parent$key = \$value");
179             }
180             }
181             }
182              
183              
184             sub load_input {
185 5     5 1 1339 my $VAR1;
186 1 50   1   225 eval read_file(@_) or die $@;
  1     1   7  
  1     1   70  
  1     1   12  
  1     1   6  
  1     1   84  
  1         261  
  1         8  
  1         63  
  1         17  
  1         7  
  1         69  
  1         256  
  1         9  
  1         72  
  1         13  
  1         3  
  1         115  
  5         47  
187              
188 5         34 return $VAR1;
189             }
190              
191             # Source: https://www.perlmonks.org/?node_id=209819
192             sub _attach_stderr_filter {
193 2     2   2642 my $pid = open(STDERR, '|-');
194 2 50       161 defined $pid or die "Cannot fork: $!\n";
195 2 100       52 return if $pid;
196              
197 1         656867 while (<STDIN>) { s/^\s*[A-Z_]+ = .*\n//g; print STDERR "$_"; }
  10         104  
  10         1059401  
198             }
199              
200             _attach_stderr_filter();
201              
202             1;
203              
204              
205             =head1 NAME
206              
207             Test::Tdd::Generator - Generate tests for existing code
208              
209             =head1 SYNOPSIS
210              
211             Add those lines inside the function you want to generate a test for:
212              
213             use Test::Tdd::Generator;
214             Test::Tdd::Generator::create_test('<test description>');
215              
216             If you also need some globals you can include them like this:
217              
218             use Test::Tdd::Generator;
219             Test::Tdd::Generator::create_test('<test description>', { globals => ['Example::'] });
220              
221             This will generate a test like this
222              
223             it '<test description>' => sub {
224             my $input = Test::Tdd::Generator::load_input(dirname(__FILE__) . "/input/MyModule_does_something.dump");
225             Test::Tdd::Generator::expand_globals($input->{globals});
226              
227             my $result = MyModule::untested_subroutine(@{$input->{args}});
228              
229             is($result, "fixme");
230             };
231              
232             =head2 Methods
233              
234             =over 4
235              
236             =item I<PACKAGE>::create_test(I<$test_description [>, I<$opts]>)
237              
238             Creates a test on the closest t/ folder, saving the original inputs that the function received as well. You can also save globals you might need passing them on $opts, check the example above
239              
240             =item I<PACKAGE>::load_input(I<$dump_file>)
241              
242             Evaluates a dump file to load the inputs on the test to be able to call the function
243              
244             =item I<PACKAGE>::expand_globals(I<$globals>)
245              
246             Expand globals that were exported to run the tests
247              
248             =back
249              
250             =cut