File Coverage

blib/lib/Test/Clustericious/Cluster.pm
Criterion Covered Total %
statement 357 383 93.2
branch 95 130 73.0
condition 43 73 58.9
subroutine 47 48 97.9
pod 16 16 100.0
total 558 650 85.8


line stmt bran cond sub pod time code
1             package Test::Clustericious::Cluster;
2              
3 48     48   3143054 use strict;
  47         165  
  47         1499  
4 38     38   1006 use warnings;
  38         125  
  38         1263  
5 35     35   949 use 5.010001;
  35         158  
6 35     35   23206 use Test2::Plugin::FauxHomeDir;
  35         2001830  
  35         313  
7 35     35   45213 use Test2::API qw( context );
  35         123  
  35         2394  
8 35     35   297 use File::Glob qw( bsd_glob );
  35         117  
  35         3709  
9 35     35   22437 use Mojo::URL;
  35         5105088  
  35         385  
10 35     35   22385 use Mojo::Loader;
  35         629350  
  35         2136  
11 35     35   23376 use Mojo::UserAgent;
  35         8431388  
  35         514  
12 35     35   2294 use Carp qw( croak carp );
  35         123  
  35         2756  
13 35     35   294 use File::Basename ();
  35         100  
  35         654  
14 35     35   228 use File::Path ();
  35         92  
  35         790  
15 35     35   26291 use Test::Mojo;
  35         1247380  
  35         557  
16              
17             # ABSTRACT: Test an imaginary beowulf cluster of Clustericious services
18             our $VERSION = '0.37'; # VERSION
19              
20              
21 35     35   26128 BEGIN { $ENV{MOJO_LOG_LEVEL} = 'fatal' }
22              
23              
24             sub new
25             {
26 28     28 1 52563 my $class = shift;
27            
28 28         93 my $args;
29 28 50 33     218 if(ref($_[0]) && eval { $_[0]->isa('Test::Mojo') })
  0         0  
30             {
31             # undocumented and deprecated
32             # you can pass in just an instance of Test::Mojo
33 0         0 carp "passing a Test::Mojo object into Test::Clustericious::Cluster->new deprecated";
34 0         0 $args = { t => $_[0] };
35             }
36             else
37             {
38 28 50       148 $args = ref $_[0] ? { %{ $_[0] } } : {@_};
  0         0  
39             }
40              
41 28 50       600 my $sep = $^O eq 'MSWin32' ? ';' : ':';
42 28         574 my $lite_path = [ split $sep, $ENV{PATH} ];
43              
44 28   100     333 $args->{lite_path} //= [];
45 28 50       151 unshift @$lite_path, ref($args->{lite_path}) ? @{ $args->{lite_path} } : ($args->{lite_path});
  28         118  
46            
47             bless {
48             t => $args->{t},
49 28         474 urls => [],
50             apps => [],
51             stopped => [],
52             index => -1,
53             url => '',
54             servers => [],
55             app_servers => [],
56             auth_url => '',
57             #extra_ua => [$t->ua],
58             lite_path => $lite_path,
59             }, $class;
60             }
61              
62              
63             sub t {
64 76     76 1 2691 my($self) = @_;
65 76   66     527 $self->{t} //= do {
66 24         294 my $t = $self->{t} = Test::Mojo->new;
67 24         619 $self->_update_default_server;
68 24         412 $t;
69             };
70             }
71              
72             sub _update_default_server
73             {
74 45     45   164 my($self) = @_;
75             $self->t->ua->server->app($self->apps->[-1])
76 45 100 66     343 if $self->{t} && @{ $self->apps } && !$self->{stopped}->[-1];
  45   66     236  
77             }
78              
79             sub _extra_ua {
80 54     54   190 my($self) = @_;
81 54   100     519 $self->{extra_ua} //= [ $self->t->ua ];
82             }
83              
84              
85 29     29 1 41610 sub urls { shift->{urls} }
86              
87              
88 179     179 1 4495 sub apps { shift->{apps} }
89              
90              
91 4     4 1 3307 sub index { shift->{index} }
92              
93              
94 49     49 1 25250 sub url { shift->{url} }
95              
96              
97 3     3 1 188 sub auth_url { shift->{auth_url} }
98              
99              
100             BEGIN {
101             # TODO
102             # so we muck with this @INC in two places. Here we do it so
103             # that you can use_ok files in your .t file. Later we do
104             # extract files in create_cluster_ok and add ~/lib to the @INC
105             # path so that we can load as regular files. This is more
106             # reliable for anything that expects a real live file, and we'd
107             # like to do that for use_ok as well in the future.
108              
109             unshift @INC, sub {
110 3381         16723531 my($self, $file) = @_;
111              
112 3381         21005 my $data = Mojo::Loader::data_section('main', "lib/$file");
113 3381 100       2005807 return unless defined $data;
114            
115             # This will make the file really there.
116             # Some stuff depends on that
117 2         24 __PACKAGE__->extract_data_section("lib/$file", 'main');
118              
119 2     5   103 open my $fh, '<', \$data;
  5         46  
  5         69  
  5         65  
120            
121             # fake out %INC because Mojo::Home freeks the heck
122             # out when it sees a CODEREF on some platforms
123             # in %INC
124 2         673 my $home = bsd_glob '~';
125 2 50       33 mkdir "$home/lib" unless -d "$home/lib";
126 2         12 $INC{$file} = "$home/lib/$file";
127            
128 2         563 return $fh;
129 35     35   23093 };
130             };
131              
132             sub _add_app_to_ua
133             {
134 70     70   555 my($self, $ua, $url, $app, $index) = @_;
135             #use Carp qw( confess );
136             #confess "usage: \$cluster->_add_app_to_ua($ua, $url, $app)" unless $url;
137 70         369 my $server = Mojo::Server::Daemon->new(
138             ioloop => $ua->ioloop,
139             silent => 1,
140             );
141 70         3786 $server->app($app);
142 70         864 $server->listen(["$url"]);
143 70         21224 $server->start;
144 70 100       295984 if(defined $index)
145             {
146 64         212 push @{ $self->{app_servers}->[$index] }, $server;
  64         425  
147             }
148             else
149             {
150 6         17 push @{ $self->{servers} }, $server;
  6         23  
151             }
152 70         327 return;
153             }
154              
155             sub _add_app
156             {
157 40     40   231 my($self, $url, $app, $index) = @_;
158 40         124 $self->_add_app_to_ua($_, $url, $app, $index) for @{ $self->_extra_ua };
  40         199  
159 40         203 return;
160             }
161              
162             sub _add_ua
163             {
164 14     14   57 my($self) = @_;
165            
166 14         37 my $max = $#{ $self->{apps} };
  14         75  
167            
168 14         185 my $ua = Mojo::UserAgent->new;
169            
170             $self->_add_app_to_ua($ua, $self->{auth_url}, $self->{auth_url})
171 14 0 33     167 if $self->{auth_url} && $self->{auth_url};
172            
173 14         73 for(my $i=0; $i<=$max; $i++)
174             {
175 20 50       89 next unless defined $self->{apps}->[$i];
176 20         58 my $stopped = $self->{stopped}->[$i];
177 20 100       66 next if $stopped;
178 19         91 $self->_add_app_to_ua($ua, $self->{urls}->[$i], $self->{apps}->[$i], $i);
179             }
180 14         44 push @{ $self->_extra_ua }, $ua;
  14         68  
181 14         394 return $ua;
182             }
183              
184             sub _load_lite_app
185             {
186 3     3   18 my($app_path, $script) = @_;
187 3         20 local @ARGV = ( eval => 'app');
188 3         14 state $index = 0;
189 3         443 eval '# line '. __LINE__ . ' "' . __FILE__ . qq("\n) . sprintf(q{
190             if(defined $script)
191             {
192             open my $fh, '>', $app_path;
193             print $fh $script;
194             close $fh;
195             }
196             package
197             Test::Clustericious::Cluster::LiteApp%s;
198             my $app = do $app_path;
199             if(!$app && (my $e = $@ || $!)) { die $e }
200             $app;
201             }, $index++);
202             }
203              
204             sub _generate_port
205             {
206 57     57   462 require IO::Socket::INET;
207 57         562 IO::Socket::INET->new(
208             Listen => 5,
209             LocalAddr => '127.0.0.1',
210             )->sockport;
211             }
212              
213             sub _create_config_helper
214             {
215 20     20   76 my($self) = @_;
216              
217 20     11   138 my $helper = sub { return $self };
  11         66  
218            
219 20         59 my $class;
220            
221 20 50       100 unless(defined $class)
222             {
223 20 50       1504 if(eval q{ require Clustericious::Config::Helpers; 1})
    0          
224             {
225 20         89 $class = 'Clustericious::Config::Helpers';
226 20         103 push @Clustericious::Config::Helpers::EXPORT, 'cluster';
227             }
228             elsif(eval q{ Clustericious::Config::Plugin; 1 })
229             {
230 0         0 $class = 'Clustericious::Config::Plugin';
231 0         0 push @Clustericious::Config::Plugin::EXPORT, 'cluster';
232             }
233             }
234            
235 20 50       208 return unless $class;
236            
237 20         72 do {
238             # there are a multitude of sins here aren't there?
239 35     35   599 no warnings 'redefine';
  35         120  
  35         2389  
240 35     35   271 no warnings 'once';
  35         122  
  35         1600  
241 35     35   326 no strict 'refs';
  35         432  
  35         92626  
242 20         65 *{join '::', $class, 'cluster'} = $helper;
  20         250  
243             };
244             }
245              
246             sub create_cluster_ok
247             {
248 20     20 1 177 my $self = shift;
249              
250 20         130 $self->_create_config_helper;
251 20         182 my $ctx = context();
252            
253 20         3292 my $total = scalar @_;
254             my @urls = map {
255 20         108 my $url = Mojo::URL->new("http://127.0.0.1");
  54         477  
256 54         22454 $url->port(_generate_port);
257 54         20702 $url } (0..$total);
258              
259 20         96 push @{ $self->{urls} }, @urls;
  20         216  
260            
261 20         67 my @errors;
262            
263 20         77 my $caller = caller;
264 20 50       116 Mojo::Loader::load_class($caller) if $caller ne 'main';
265              
266 20         171 local @INC = @INC;
267 20         241 $self->extract_data_section(qr{^lib/}, $caller);
268 20         872 my $home = bsd_glob '~';
269 20 100       490 unshift @INC, "$home/lib"
270             if -d "$home/lib";
271              
272 20         149 foreach my $i (0..$#_)
273             {
274 34         271 $self->{index}++;
275 34         161 $self->{url} = shift @urls;
276            
277 34         114 my $app_name;
278             my $cb;
279 34         117 my $config = {};
280 34         154 my $item = $_[$i];
281 34 100       181 unless(ref $item)
282             {
283 31         96 my $fn = $item;
284 31         140 $fn =~ s{::}{-}g;
285 31         138 $fn = "etc/$fn.conf";
286 31 100       219 if(Mojo::Loader::data_section($caller, $fn))
287             {
288 6         125 $item = [ $item, Mojo::Loader::data_section($caller, $fn) ];
289             }
290             }
291              
292 34 100       661 if(ref $item eq 'ARRAY')
293             {
294 9         26 ($app_name, $config, $cb) = @{ $item };
  9         44  
295 9 100       49 unless(ref $config)
296             {
297 6         156 my $home = bsd_glob '~';
298 6 100       193 mkdir "$home/etc" unless -d "$home/etc";
299 6         49 open my $fh, '>', do {
300 6         21 my $fn = $app_name;
301 6         26 $fn =~ s{::}{-}g;
302 6         468 $fn = "$home/etc/$fn.conf";
303             };
304 6         63 print $fh $config;
305 6         200 close $fh;
306 6         46 $config = {};
307             }
308             }
309             else
310             {
311 25         82 $app_name = $item;
312             }
313 34 50       301 my $psgi_name = $app_name =~ /\.psgi$/ ? $app_name : "$app_name.psgi";
314              
315            
316 34         113 my $app;
317             # we want to try to load class first, so that YourApp.pm
318             # will be picked over lite app yourapp on MSWin32
319             # (which is case insensative). So we save the error
320             # (from $@) and only push it onto the @errors list
321             # if loading as a lite app also fails.
322             my $first_try_error;
323            
324 34 50       140 unless(defined $app)
325             {
326 34     23   3261 $app = eval qq{
  23         9318  
  20         16580  
  20         313  
327             use $app_name;
328             if($app_name->isa('Clustericious::App'))
329             {
330             eval { # if they have Clustericious::Log 0.11 or better
331             require Test::Clustericious::Log;
332             Test::Clustericious::Log->import;
333             };
334             }
335             $app_name->new(\$config);
336             };
337 34         82517 $first_try_error = $@;
338             }
339              
340 34 100       224 unless(defined $app)
341             {
342 4 100       32 if(my $script = Mojo::Loader::data_section($caller, "script/$app_name"))
343             {
344 1         41 my $home = bsd_glob '~';
345 1 50       135 mkdir "$home/script" unless -d "$home/script";
346 1         12 $app = _load_lite_app("$home/script/$app_name", $script);
347 1 50       17 if(my $error = $@)
348 0         0 { push @errors, [ $app_name, $error ] }
349             }
350            
351 4 50       116 if(my $script = Mojo::Loader::data_section($caller, "script/$psgi_name"))
352             {
353 0         0 my $home = bsd_glob '~';
354 0         0 require Mojolicious;
355 0         0 require Mojolicious::Plugin::MountPSGI;
356 0         0 $app = Mojolicious->new;
357             # TODO: check syntax of .psgi file?
358 0         0 $self->extract_data_section("script/$psgi_name", $caller);
359 0         0 $app->plugin('Mojolicious::Plugin::MountPSGI' => { '/' => "$home/script/$psgi_name" });
360             }
361             }
362            
363 34 100       297 unless(defined $app)
364             {
365 3         9 foreach my $dir (@{ $self->{lite_path} })
  3         31  
366             {
367 11 100 66     291 if($app_name !~ /\.psgi$/
      66        
368             && (($^O eq 'MSWin32' && -e "$dir/$app_name") || (-x "$dir/$app_name")))
369             {
370 2         20 $app = _load_lite_app("$dir/$app_name");
371 2 50       11 if(my $error = $@)
372 0         0 { push @errors, [ $app_name, $error ] }
373 2         6 last;
374             }
375            
376 9 50       150 if(-e "$dir/$psgi_name")
377             {
378 0         0 require Mojolicious;
379 0         0 require Mojolicious::Plugin::MountPSGI;
380 0         0 $app = Mojolicious->new;
381 0         0 $app->plugin('Mojolicious::Plugin::MountPSGI' => { '/' => "$dir/$psgi_name" });
382             }
383             }
384             }
385            
386 34 100       163 unless(defined $app)
387             {
388 1 50       8 push @errors, [ $app_name, $first_try_error ]
389             if $first_try_error;
390             }
391            
392 34         135 push @{ $self->apps }, $app;
  34         211  
393 34 100       235 if(defined $app)
394 33         181 { $self->_add_app($self->url, $app, $#{ $self->apps }); }
  33         132  
395            
396 34 100       122 if(eval { $app->isa('Clustericious::App') })
  34         654  
397             {
398 12 50       192 if($app->can('auth_ua'))
399             {
400             $app->helper(auth_ua => sub {
401             die "no plug auth service configured for test cluster, either turn off authentication or use Test::Clustericious::Cluster#create_plugauth_lite_ok"
402 0 0   0   0 unless defined $self->{auth_ua};
403 0         0 $self->{auth_ua};
404 0         0 });
405             }
406             }
407              
408 34 100 100     428 if($app && $app->isa('Clustericious::App'))
409             {
410 12         41 my $fn = $app_name;
411 12         55 $fn =~ s{::}{-}g;
412 12         60 $fn = "$home/etc/$fn.conf";
413 12 50       350 mkdir "$home/etc" unless -d "$home/etc";
414 12 100       209 unless(-e $fn)
415             {
416             # YAML::XS is a prereq for Clustericious
417             # so we can use it here.
418 4         50 require YAML::XS;
419 4         11 my %config = %{ $app->config };
  4         32  
420 4   66     106 $config{url} //= $self->url->to_string;
421 4         511 my $payload = YAML::XS::Dump(\%config);
422 4         389 open my $fh, '>', $fn;
423 4         56 print $fh $payload;
424 4         204 close $fh;
425             }
426             }
427              
428 34 50       238 $cb->() if defined $cb;
429             }
430              
431 20         224 $ctx->ok(@errors == 0, "created cluster");
432 20         10944 $ctx->diag("exception: " . $_->[0] . ': ' . $_->[1]) for @errors;
433            
434 20 100       249 if($INC{'Clustericious/App.pm'})
435             {
436 7         25 eval { require Clustericious::Client };
  7         180  
437 7 50 33     60424 if(!$@ && Clustericious::Client->can('_mojo_user_agent_factory'))
438             {
439             Clustericious::Client->_mojo_user_agent_factory(sub {
440 6     6   3971 $self->create_ua;
441 7         72 });
442             }
443             }
444            
445 20 50       278 if(defined $self->{t})
446             {
447 20         117 $self->_update_default_server;
448             }
449            
450 20         431 $ctx->release;
451 20         1100 return $self;
452             }
453              
454              
455             sub create_plugauth_lite_ok
456             {
457 4     4 1 5141 my($self, %args) = @_;
458 4         11 my $ok = 1;
459 4         18 my $ctx = context();
460            
461 4 50 33     606 if(eval q{ use Clustericious; 1 } && ! eval q{ use Clustericious 0.9925; 1 })
462             {
463 0         0 croak "creat_plugin_lite_ok requires Clustericious 0.9925 or better (see Test::Clustericious::Test for details)";
464             }
465              
466 4         14 my @diag;
467              
468 4 100 66     44 if($self->{auth_ua} || $self->{auth_url})
469             {
470 1         3 $ok = 0;
471 1         4 push @diag, 'only use create_plugauth_lite_ok once';
472             }
473             else
474             {
475 3         15 my $ua = $self->{auth_ua} = $self->_add_ua;
476 3         22 my $url = Mojo::URL->new("http://127.0.0.1");
477 3         1648 $url->port(_generate_port);
478            
479 3         1284 eval {
480 3         35 require PlugAuth::Lite;
481            
482 3         33 my $app = $self->{auth_app} = PlugAuth::Lite->new(\%args);
483 3         45 $self->{auth_url} = $url;
484 3         18 $self->_add_app($url, $app);
485             };
486 3 50       17 if(my $error = $@)
487             {
488 0         0 $ctx->diag("error: $error");
489 0         0 $ok = 0;
490             }
491             }
492            
493 4         31 $ctx->ok($ok, "PlugAuth::Lite instance on " . $self->{auth_url});
494 4         1551 $ctx->diag($_) for @diag;
495 4         74 $ctx->release;
496            
497 4         159 return $self;
498             }
499              
500              
501             sub stop_ok
502             {
503 5     5 1 22178 my($self, $index, $test_name) = @_;
504 5         17 my $ok = 1;
505 5         23 my $ctx = context();
506            
507 5         474 my $error;
508            
509 5         29 my $app = $self->apps->[$index];
510 5 100       25 if(defined $app)
511             {
512 4         15 my $app_name = ref $app;
513 4   33     51 $test_name //= "stop service $app_name ($index)";
514 4         10 $_->stop for @{ $self->{app_servers}->[$index] };
  4         39  
515 4         1763 eval { @{ $self->{app_servers}->[$index] } = () };
  4         13  
  4         25  
516 4         1337 $error = $@;
517 4 50       20 $ok = 0 if $error;
518            
519 4 50 66     12 if($index == $#{ $self->apps } && $self->{t})
  4         14  
520             {
521 1         7 $self->t->ua->server(Mojo::UserAgent::Server->new);
522             }
523            
524             }
525             else
526             {
527 1         4 $error = "no such app for index: $index";
528 1         2 $ok = 0;
529             }
530            
531 5 100       1189 $self->{stopped}->[$index] = 1 if $ok;
532            
533 5   66     25 $test_name //= "stop service ($index)";
534            
535 5         23 my $ret = $ctx->ok($ok, $test_name);
536            
537 5 100       1195 $ctx->diag($error) if $error;
538 5         107 $ctx->release;
539            
540 5         164 $ret;
541             }
542              
543              
544             sub start_ok
545             {
546 5     5 1 6801 my($self, $index, $test_name) = @_;
547 5         16 my $ok = 1;
548 5         21 my $ctx = context();
549            
550 5         497 my $error;
551              
552 5         24 my $app = $self->apps->[$index];
553 5 100       25 if(defined $app)
554             {
555 4         15 my $app_name = ref $app;
556 4   33     42 $test_name //= "start service $app_name ($index)";
557 4         10 eval {
558 4         19 $self->_add_app($self->urls->[$index], $app, $index);
559             };
560 4 50       27 if(my $error = $@)
561             {
562 0         0 $ctx->diag("error in start: $error");
563 0         0 $ok = 0;
564             }
565              
566             }
567             else
568             {
569 1         3 $error = "no such app for index: $index";
570 1         2 $ok = 0;
571             }
572            
573 5 100       28 $self->{stopped}->[$index] = 0 if $ok;
574              
575 5 50 66     12 if($index == $#{ $self->apps } && $self->{t})
  5         23  
576             {
577 1         7 $self->_update_default_server;
578             }
579              
580 5   66     43 $test_name //= "start service ($index)";
581            
582 5         34 $ctx->ok($ok, $test_name);
583 5 100       1379 $ctx->diag($error) if $error;
584 5         113 $ctx->release;
585            
586 5         188 $ok;
587             }
588              
589              
590             sub is_stopped
591             {
592 6     6 1 24010 my($self, $index, $test_name) = @_;
593            
594 6         20 my $ok = !!$self->{stopped}->[$index];
595            
596 6   33     46 $test_name //= "servers ($index) is stopped";
597            
598 6         19 my $ctx = context();
599 6         498 $ctx->ok($ok, $test_name);
600 6         1209 $ctx->release;
601 6         168 $ok;
602             }
603              
604              
605             sub isnt_stopped
606             {
607 11     11 1 14397 my($self, $index, $test_name) = @_;
608            
609 11         36 my $ok = !$self->{stopped}->[$index];
610            
611 11   33     85 $test_name //= "servers ($index) is not stopped";
612            
613 11         31 my $ctx = context();
614 11         907 $ctx->ok($ok, $test_name);
615 11         2513 $ctx->release;
616            
617 11         304 $ok;
618             }
619              
620              
621             sub create_ua
622             {
623 9     9 1 14775 shift->_add_ua;
624             }
625              
626              
627             sub extract_data_section
628             {
629 26     26 1 8506 my($class, $regex, $caller) = @_;
630              
631 26   66     149 $regex //= qr{};
632            
633 26 100       168 unless(ref $regex eq 'Regexp')
634             {
635 2         23 $regex = quotemeta $regex;
636 2         39 $regex = qr{^$regex$};
637             }
638            
639 26   66     141 $caller //= caller;
640 26         169 my $all = Mojo::Loader::data_section $caller;
641 26         1647 my $home = bsd_glob '~';
642 26         194 my $ctx = context();
643              
644 26         2778 foreach my $name (keys %$all)
645             {
646 35     35   951 use autodie;
  35         354963  
  35         247  
647 52 100       17756 next unless $name =~ $regex;
648 41         2039 my $basename = File::Basename::basename $name;
649 41         1234 my $dir = File::Basename::dirname $name;
650              
651 41 100       1688 unless(-d "$home/$dir")
652             {
653 28         336 $ctx->note("[extract] DIR $home/$dir");
654 28         17612 File::Path::mkpath "$home/$dir", 0, 0700;
655             }
656 41 100       1214 unless(-f "$home/$dir/$basename")
657             {
658 39         411 $ctx->note("[extract] FILE $home/$dir/$basename");
659 39         10936 open my $fh, '>', "$home/$dir/$basename";
660 39         52142 print $fh $all->{$name};
661 39         226 close $fh;
662             }
663             }
664            
665 26         8718 $ctx->release;
666 26         779 $class;
667             }
668              
669              
670             sub client
671             {
672 6     6 1 21586 my($self, $n) = @_;
673 6         33 my $app = $self->apps->[$n];
674 6 100       26 return unless eval { $app->isa('Clustericious::App') };
  6         64  
675 5         26 my $client_class = ref($app) . '::Client';
676 5 100 66     164 unless($client_class->can('new') || eval qq{ require $client_class; $client_class->isa('Clustericious::Client') })
677             {
678 1         10 require Clustericious::Client;
679 1         5 $client_class = 'Clustericious::Client';
680             }
681 5         17 my %config = %{ $app->config };
  5         34  
682 5         120 $config{url} = $self->urls->[$n];
683 5         44 my $client = $client_class->new( config => Clustericious::Config->new(\%config) );
684             }
685              
686             1;
687              
688             __END__
689              
690             =pod
691              
692             =encoding UTF-8
693              
694             =head1 NAME
695              
696             Test::Clustericious::Cluster - Test an imaginary beowulf cluster of Clustericious services
697              
698             =head1 VERSION
699              
700             version 0.37
701              
702             =head1 SYNOPSIS
703              
704             use Test2::Bundle::More;
705             use Test::Clustericious::Cluster;
706            
707             # suppose MyApp1 isa Clustericious::App and
708             # MyApp2 is a Mojolicious app
709             my $cluster = Test::Clustericious::Cluster->new;
710             $cluster->create_cluster_ok('MyApp1', 'MyApp2');
711            
712             my @urls = @{ $cluster->urls };
713             my $t = $cluster->t; # an instance of Test::Mojo
714            
715             $t->get_ok("$url[0]/arbitrary_path"); # tests against MyApp1
716             $t->get_ok("$url[1]/another_path"); # tests against MyApp2
717            
718             done_testing;
719            
720             __DATA__
721            
722             @@ etc/MyApp1.conf
723             ---
724             # Clustericious configuration
725             url: <%= cluster->url %>
726             url_for_my_app2: <%= cluster->urls->[1] %>
727              
728             =head1 DESCRIPTION
729              
730             This module allows you to test an entire cluster of Clustericious services
731             (or just one or two). The only prerequisites are L<Mojolicious>, and
732             L<Test2::Plugin::FauxHomeDir> so you can mix and match L<Mojolicious>,
733             L<Mojolicious::Lite> and full L<Clustericious> apps and test how they interact.
734              
735             If you are testing against Clustericious applications, it is important to
736             either use this module as early as possible, or use L<Test2::Plugin::FauxHomeDir>
737             as the very first module in your test, as testing Clustericious configurations
738             depend on the testing home directory being setup by L<Test2::Plugin::FauxHomeDir>.
739              
740             In addition to passing L<Clustericious> configurations into the
741             C<create_cluster_ok> method as describe below, you can include configuration
742             in the data section of your test script. The configuration files use
743             L<Clustericious::Config>, so you can use L<Mojo::Template> directives to
744             embed Perl code in the configuration. You can access the L<Test::Clustericious::Cluster>
745             instance from within the configuration using the C<cluster> function, which
746             can be useful for getting the URL for the your and other service URLs.
747              
748             __DATA__
749            
750             @@ etc/Foo.conf
751             ---
752             url <%= cluster->url %>
753             % # because YAML is (mostly) a super set of JSON you can
754             % # convert perl structures into config items using json
755             % # function:
756             % # (json method requires Clustericious::Config 0.25)
757             other_urls: <%= json [ @{ cluster->urls } ] %>
758              
759             You can also put perl code in the data section of your test file, which
760             can be useful if there isn't a another good place to put it. This
761             example embeds as L<Mojolicious> app "FooApp" and a L<Clustericious::App>
762             "BarApp" into the test script itself:
763              
764             ...
765             $cluster->create_cluster_ok('FooApp', 'BarApp');
766             ...
767            
768             __DATA__
769            
770             @@ lib/FooApp.pm
771             package FooApp;
772            
773             # FooApp is a Mojolicious app
774            
775             use Mojo::Base qw( Mojolicious );
776            
777             sub startup
778             {
779             shift->routes->get('/' => sub { shift->render(text => 'hello there from foo') });
780             }
781            
782             1;
783            
784             @@ lib/BarApp.pm
785             package BarApp;
786            
787             # BarApp is a Clustericious::App
788            
789             use strict;
790             use warnings;
791             use base qw( Clustericious::App );
792            
793             1;
794            
795             @@ lib/BarApp/Routes.pm
796             package BarApp::Routes;
797            
798             use strict;
799             use warnings;
800             use Clustericious::RouteBuilder;
801            
802             get '/' => sub { shift->render(text => 'hello there from bar') };
803            
804             1;
805              
806             These examples are full apps, but you could also use this
807             feature to implement mocks to test parts of your program
808             that use resources that aren't easily available during
809             unit testing, or may change from host to host. Here is an
810             example that mocks parts of L<Net::hostent>:
811              
812             use strict;
813             use warnings;
814             use Test::Clustericious::Cluster;
815             use Test2::Bundle::More;
816            
817             eval q{ use Net::hostent };
818             is $@, '';
819            
820             is gethost('bar')->name, 'foo.example.com', 'gethost(bar).name = foo.example.com';
821            
822             done_testing;
823            
824             __DATA__
825            
826             @@ lib/Net/hostent.pm
827             package Net::hostent;
828            
829             use strict;
830             use warnings;
831             use base qw( Exporter );
832             our @EXPORT = qw( gethost );
833            
834             sub gethost
835             {
836             my $input_name = shift;
837             return unless $input_name =~ /^(foo|bar|baz|foo.example.com)$/;
838             bless {}, 'Net::hostent';
839             }
840            
841             sub name { 'foo.example.com' }
842             sub aliases { qw( foo.example.com foo bar baz ) }
843            
844             1;
845              
846             =head1 CONSTRUCTOR
847              
848             =head2 new
849              
850             my $cluster = Test::Clustericious::Cluster->new( %args )
851              
852             Arguments:
853              
854             =head3 t
855              
856             The Test::Mojo object to use.
857             If not provided, then a new one will be created.
858              
859             =head3 lite_path
860              
861             List reference of paths to search for L<Mojolicious::Lite>
862             or PSGI apps.
863              
864             =head1 ATTRIBUTES
865              
866             =head2 t
867              
868             my $t = $cluster->t;
869              
870             The instance of Test::Mojo used in testing.
871              
872             =head2 urls
873              
874             my @urls = @{ $cluster->urls };
875              
876             The URLs for the various services.
877             Returned as an array ref.
878              
879             =head2 apps
880              
881             my @apps = @{ $cluster->apps };
882              
883             The application objects for the various services.
884             Returned as an array ref.
885              
886             =head2 index
887              
888             my $index = $cluster->index;
889              
890             The index of the current app (used from within a
891             L<Clustericious::Config> configuration.
892              
893             =head2 url
894              
895             my $url = $cluster->url;
896              
897             The url of the current app (used from within a
898             L<Clustericious::Config> configuration.
899              
900             =head2 auth_url
901              
902             my $url = $cluster->auth_url;
903              
904             The URL for the PlugAuth::Lite service, if one has been started.
905              
906             =head1 METHODS
907              
908             =head2 create_cluster_ok
909              
910             $cluster->create_cluster_ok( @services )
911              
912             Adds the given services to the test cluster.
913             Each element in the services array may be either
914              
915             =over 4
916              
917             =item string
918              
919             The string is taken to be the L<Mojolicious> or L<Clustericious>
920             application class name. No configuration is created or passed into
921             the App.
922              
923             This can also be the name of a L<Mojolicious::Lite> application.
924             The PATH environment variable will be used to search for the
925             lite application. The script for the lite app must be executable.
926             You can specify additional directories to search using the
927             C<lite_path> argument to the constructor.
928              
929             This can also be a PSGI application. In this case it needs to be
930             in the C<__DATA__> section of your test and it must have a name
931             in the form C<script/app.psgi>. This also requires
932             L<Mojolicious::Plugin::MountPSGI> already be installed so if you
933             use this feature make sure you declare that as a prereq.
934              
935             =item list reference in the form: [ string, hashref ]
936              
937             The string is taken to be the L<Mojolicious> application name.
938             The hashref is the configuration passed into the constructor
939             of the app. This form should NOT be used for L<Clustericious>
940             apps (see the third form).
941              
942             =item list reference in the form: [ string, string ]
943              
944             The first string is taken to be the L<Clustericious> application
945             name. The second string is the configuration in either YAML
946             or JSON format (may include L<Mojo::Template> templating in it,
947             see L<Clustericious::Config> for details). This form requires
948             that you have L<Clustericous> installed, and of course should
949             not be used for non-L<Clustericious> L<Mojolicious> applications.
950              
951             =back
952              
953             =head2 create_plugauth_lite_ok
954              
955             $cluster->create_plugauth_lite_ok( %args )
956              
957             Add a L<PlugAuth::Lite> service to the test cluster. The
958             C<%args> are passed directly into the L<PlugAuth::Lite>
959             constructor.
960              
961             You can retrieve the URL for the L<PlugAuth::Lite> service
962             using the C<auth_url> attribute.
963              
964             This feature requires L<PlugAuth::Lite> and L<Clustericious>
965             0.9925 or better, though neither are a prerequisite of this
966             module. If you are using this method you need to either require
967             L<PlugAuth::Lite> and L<Clustericious> 0.9925 or better, or skip
968             your test in the event that the user has an earlier version.
969             For example:
970              
971             use strict;
972             use warnings;
973             use Test::Clustericious::Cluster;
974             use Test2::Bundle::More;
975             BEGIN {
976             skip_all 'test requires Clustericious 0.9925'
977             unless eval q{ use Clustericious 1.00; 1 };
978             skip_all 'test requires PlugAuth::Lite'
979             unless eval q{ use PlugAuth::Lite 0.30; 1 };
980             };
981              
982             =head2 stop_ok
983              
984             $cluster->stop_ok( $index );
985             $cluster->stop_ok( $index, $test_name);
986              
987             Stop the given service. The service is specified by
988             an index, the first application when you created the
989             cluster is 0, the second is 1, and so on.
990              
991             See L<CAVEATS|Test::Clustericious::Cluster#CAVEATS>
992             below on interactions with IPv6 or TLS/SSL.
993              
994             =head2 start_ok
995              
996             $cluster->start_ok( $index );
997             $cluster->start_ok( $index, $test_name );
998              
999             Start the given service. The service is specified by
1000             an index, the first application when you created the
1001             cluster is 0, the second is 1, and so on.
1002              
1003             =head2 is_stopped
1004              
1005             $cluster->is_stopped( $index );
1006             $cluster->is_stopped( $index, $test_name );
1007              
1008             Passes if the given service is stopped.
1009              
1010             =head2 isnt_stopped
1011              
1012             $cluster->isnt_stopped( $index );
1013             $cluster->isnt_stopped( $index, $test_name );
1014              
1015             Passes if the given service is not stopped.
1016              
1017             =head2 create_ua
1018              
1019             my $ua = $cluster->create_ua;
1020              
1021             Create a new instance of Mojo::UserAgent which can be used
1022             to connect to nodes in the test cluster.
1023              
1024             =head2 extract_data_section
1025              
1026             $cluster->extract_data_section($regex);
1027             Test::Clustericious::Cluster->extract_data_section($regex);
1028              
1029             Extract the files from the data section of the current package
1030             that match the given regex. C<$regex> can also be a plain
1031             string for an exact filename match.
1032              
1033             =head2 client
1034              
1035             my $client = $cluster->client($n);
1036              
1037             Return a L<Clustericious::Client> object for use with the C<$n>th
1038             service in the cluster. If there is a corresponding C<YourService::Client>
1039             class then it will be used. Otherwise you will get a generic
1040             L<Clustericious::Client> object with the correct URL configured.
1041              
1042             This method only works with L<Clustericious> services.
1043              
1044             =head1 CAVEATS
1045              
1046             Some combination of Mojolicious, FreeBSD, IPv6 and TLS/SSL
1047             seem to react badly to the use of
1048             L<stop_ok|Test::Clustericious::Cluster#stop_ok>. The work
1049             around is to turn IPv6 and TLS/SSL off in the beginning
1050             of any tests that uses stop_ok your test like thus:
1051              
1052             use strict;
1053             use warnings;
1054             BEGIN { $ENV{MOJO_NO_IPV6} = 1; $ENV{MOJO_NO_TLS} = 1; }
1055             use Test::Clustericious::Cluster;
1056              
1057             A proper fix would be desirable, see
1058              
1059             https://github.com/plicease/Test-Clustericious-Cluster/issues/3
1060              
1061             If you want to help.
1062              
1063             =head1 AUTHOR
1064              
1065             Graham Ollis <plicease@cpan.org>
1066              
1067             =head1 COPYRIGHT AND LICENSE
1068              
1069             This software is copyright (c) 2013 by Graham Ollis.
1070              
1071             This is free software; you can redistribute it and/or modify it under
1072             the same terms as the Perl 5 programming language system itself.
1073              
1074             =cut