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 39 my ($level) = @_;
6 13         248 my @foo = caller( $level + 2 );
7 13         90 return @DB::args;
8             }
9              
10             package Test::Tdd::Generator;
11              
12 2     2   3142 use strict;
  2         4  
  2         48  
13 2     2   8 use warnings;
  2         2  
  2         64  
14              
15 2     2   26 use File::Basename qw(dirname basename);
  2         4  
  2         210  
16 2     2   10 use File::Path qw(make_path);
  2         4  
  2         146  
17 2     2   966 use File::Slurp qw(read_file write_file);
  2         63824  
  2         108  
18 2     2   5480 use Term::ANSIColor;
  2         15728  
  2         120  
19 2     2   1140 use Data::Dumper;
  2         13232  
  2         2488  
20              
21              
22             sub create_test {
23 13     13 1 38 my ($test_description, $opts) = @_;
24              
25 13         292 my ($package, $filename) = caller(0);
26 13         212 my ($_package, $_filename, $_line, $subroutine) = caller(1);
27 13         98 my ($test_path, $lib_path) = _find_test_and_lib_folders($filename);
28              
29 13         34 my $actual_test_path;
30 13 50       148 if (not -w $test_path) {
31 0         0 $actual_test_path = $test_path;
32 0         0 $test_path = "/tmp/t";
33             }
34              
35 13         59 my $test_file = $filename;
36 13         119 $test_file =~ s/$lib_path//;
37 13         203 $test_file =~ s/\.pm$/\.t/;
38 13         36 $test_file = $test_path . $test_file;
39              
40 13 50 66     339 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         2367 make_path dirname($test_file);
45              
46 13         153 my @args = DB::called_args(0);
47 13         32 my $globals = {};
48 13 100       247 $globals = _get_globals($opts->{globals}) if defined $opts->{globals};
49 13         109 my $input = { args => \@args, globals => $globals };
50 13         51 my $input_file = _save_input($test_file, $test_description, $input);
51              
52 13         67 my $global_expansion = "";
53 13 100       81 $global_expansion = "\n Test::Tdd::Generator::expand_globals(\$input->{globals});\n" if defined $opts->{globals};
54 13         59 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         43 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       237 if (-e $test_file) {
80 4         17 $content = read_file($test_file);
81 4         491 $content =~ s/(\};\n\nruntests)/$test_body$1/;
82             }
83              
84 13         56 write_file($test_file, $content);
85              
86 13         1760 print _get_instructions($test_file, $test_body, $test_path, $actual_test_path);
87             }
88              
89              
90             sub _get_instructions {
91 13     13   96 my ($test_file, $test_body, $test_path, $actual_test_path) = @_;
92              
93 13         151 my $run_instructions = color("green") . "Run it with:" . color("reset") . "\n\n provetdd $test_file\n\n";
94 13         690 my $move_instructions = "";
95 13 50       29 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         64 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   15698 my ($path) = @_;
107              
108 14         1052 my $dir = dirname($path);
109 14         77 my $previous = $dir;
110 14         60 while ($dir ne '.') {
111 42         78 my $test_folder = "$dir/t";
112 42 100       925 return ($test_folder, $previous) if -d $test_folder;
113 28         72 $previous = $dir;
114 28         686 $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   86 my ($test_file, $test_description, $input) = @_;
122              
123 13         467 my $inputs_folder = dirname($test_file) . '/input';
124 13         1279 make_path $inputs_folder;
125 13         152 $test_description =~ s/ /_/g;
126 13         524 my $test_file_base = basename($test_file, ".t");
127 13         37 my $input_file = "$test_file_base\_$test_description.dump";
128 13         26 my $input_file_path = "$inputs_folder/$input_file";
129              
130 13         105 local $Data::Dumper::Deparse = 1;
131 13         24 local $Data::Dumper::Maxrecurse = 0;
132 13         118 my $dumped = Dumper($input);
133 13         8517 $dumped =~ s/use strict/no strict/g;
134 13         123 write_file($input_file_path, $dumped);
135              
136 13         3464 return $input_file;
137             }
138              
139              
140             sub _test_exists {
141 4     4   29 my ($test_file, $test_description) = @_;
142              
143 4         62 my $content = read_file($test_file);
144 4         545 return $content =~ /it '$test_description'/;
145             }
146              
147              
148             sub _get_globals {
149 12     12   54315 my ($globals_names) = @_;
150              
151 12         140 return { map { $_ => _get_global_var($_) } @$globals_names };
  12         211  
152             }
153              
154              
155             sub _get_global_var {
156 25     25   63 my $name = shift;
157              
158 25         1980 my $global_var = eval "\$$name";
159 25 100       122 if ($global_var) {
160 13         108 return $global_var;
161             } else {
162 12         539 my %global_map = eval "\%$name";
163 12         69 %global_map = map { ($_ => _get_global_var($name . $_) ) } (keys %global_map);
  13         64  
164 12         139 return \%global_map;
165             }
166             }
167              
168              
169             sub expand_globals {
170 7     7 1 44393 my ($globals, $parent) = @_;
171 7   100     248 $parent ||= '';
172              
173 7         6 for my $key (keys %{$globals}) {
  7         49  
174 7         21 my $value = $globals->{$key};
175 7 100       64 if ($key =~ /::$/) {
176 4         50 expand_globals($value, $parent . $key);
177             } else {
178 3         621 eval("\$$parent$key = \$value");
179             }
180             }
181             }
182              
183              
184             sub load_input {
185 5     5 1 985 my $VAR1;
186 1 50   1   229 eval read_file(@_) or die $@;
  1     1   5  
  1     1   68  
  1     1   10  
  1     1   5  
  1     1   68  
  1         187  
  1         6  
  1         65  
  1         8  
  1         7  
  1         60  
  1         202  
  1         3  
  1         55  
  1         9  
  1         3  
  1         107  
  5         29  
187              
188 5         21 return $VAR1;
189             }
190              
191             # Source: https://www.perlmonks.org/?node_id=209819
192             sub _attach_stderr_filter {
193 2     2   2052 my $pid = open(STDERR, '|-');
194 2 50       127 defined $pid or die "Cannot fork: $!\n";
195 2 100       50 return if $pid;
196              
197 1         649003 while (<STDIN>) { s/^\s*[A-Z_]+ = .*\n//g; print STDERR "$_"; }
  10         88  
  10         931581  
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