File Coverage

blib/lib/Test/Consul.pm
Criterion Covered Total %
statement 54 169 31.9
branch 4 64 6.2
condition 2 18 11.1
subroutine 17 33 51.5
pod 8 9 88.8
total 85 293 29.0


line stmt bran cond sub pod time code
1             package Test::Consul;
2             $Test::Consul::VERSION = '0.015';
3             # ABSTRACT: Run a consul server for testing
4              
5 4     4   2251 use 5.010;
  4         31  
6 4     4   1889 use namespace::autoclean;
  4         73601  
  4         13  
7              
8 4     4   2445 use File::Which qw(which);
  4         4432  
  4         249  
9 4     4   1424 use JSON::MaybeXS qw(JSON encode_json);
  4         28942  
  4         232  
10 4     4   3381 use Path::Tiny;
  4         45657  
  4         216  
11 4     4   2172 use POSIX qw(WNOHANG);
  4         25345  
  4         19  
12 4     4   5840 use Carp qw(croak);
  4         9  
  4         186  
13 4     4   2926 use HTTP::Tiny v0.014;
  4         169291  
  4         166  
14 4     4   2058 use Net::EmptyPort qw( check_port );
  4         16109  
  4         257  
15 4     4   2981 use File::Temp qw( tempfile );
  4         44087  
  4         271  
16 4     4   31 use Scalar::Util qw( blessed );
  4         14  
  4         168  
17              
18 4     4   2275 use Moo;
  4         32241  
  4         20  
19 4     4   8526 use Types::Standard qw( Bool Enum Undef );
  4         304921  
  4         35  
20 4     4   6229 use Types::Common::Numeric qw( PositiveInt PositiveOrZeroInt );
  4         50776  
  4         28  
21 4     4   4104 use Types::Common::String qw( NonEmptySimpleStr );
  4         173317  
  4         69  
22              
23             my $start_port = 49152;
24             my $current_port = $start_port;
25             my $end_port = 65535;
26              
27             sub _unique_empty_port {
28 0     0   0 my ($udp_too) = @_;
29              
30 0         0 my $port = 0;
31 0         0 while ($port == 0) {
32 0         0 $current_port ++;
33 0 0       0 $current_port = $start_port if $current_port > $end_port;
34 0 0       0 next if check_port( $current_port, 'tcp' );
35 0 0 0     0 next if $udp_too and check_port( $current_port, 'udp' );
36 0         0 $port = $current_port;
37             }
38              
39             # Make sure we return a scalar with just numeric data so it gets
40             # JSON encoded without quotes.
41 0         0 return $port;
42             }
43              
44             has _pid => (
45             is => 'rw',
46             predicate => '_has_pid',
47             clearer => '_clear_pid',
48             );
49              
50             has port => (
51             is => 'lazy',
52             isa => PositiveInt,
53             );
54             sub _build_port {
55 0     0   0 return _unique_empty_port();
56             }
57              
58             has serf_lan_port => (
59             is => 'lazy',
60             isa => PositiveInt,
61             );
62             sub _build_serf_lan_port {
63 0     0   0 return _unique_empty_port( 1 );
64             }
65              
66             has serf_wan_port => (
67             is => 'lazy',
68             isa => PositiveInt,
69             );
70             sub _build_serf_wan_port {
71 0     0   0 return _unique_empty_port( 1 );
72             }
73              
74             has server_port => (
75             is => 'lazy',
76             isa => PositiveInt,
77             );
78             sub _build_server_port {
79 0     0   0 return _unique_empty_port();
80             }
81              
82             has node_name => (
83             is => 'lazy',
84             isa => NonEmptySimpleStr,
85             );
86             sub _build_node_name {
87 0     0   0 state $node_num = 0;
88 0         0 $node_num++;
89 0         0 return "tc_node$node_num";
90             }
91              
92             has datacenter => (
93             is => 'lazy',
94             isa => NonEmptySimpleStr,
95             );
96             sub _build_datacenter {
97 0     0   0 state $dc_num = 0;
98 0         0 $dc_num++;
99 0         0 return "tc_dc$dc_num";
100             }
101              
102             has enable_acls => (
103             is => 'ro',
104             isa => Bool,
105             );
106              
107             has acl_default_policy => (
108             is => 'ro',
109             isa => Enum[qw( allow deny )],
110             default => 'allow',
111             );
112              
113             has acl_master_token => (
114             is => 'ro',
115             isa => NonEmptySimpleStr,
116             default => '01234567-89AB-CDEF-GHIJ-KLMNOPQRSTUV',
117             );
118              
119             has enable_remote_exec => (
120             is => 'ro',
121             isa => Bool,
122             );
123              
124             has bin => (
125             is => 'lazy',
126             isa => NonEmptySimpleStr | Undef,
127             );
128             sub _build_bin {
129 0     0   0 my ($self) = @_;
130 0         0 return $self->found_bin;
131             }
132              
133             has datadir => (
134             is => 'ro',
135             isa => NonEmptySimpleStr,
136             predicate => 1,
137             );
138              
139             has version => (
140             is => 'lazy',
141             isa => PositiveOrZeroInt,
142             default => sub {
143             my ($self) = @_;
144             return $self->found_version;
145             },
146             );
147              
148             sub running {
149 0     0 1 0 my ($self) = @_;
150 0         0 return !!$self->_has_pid();
151             }
152              
153             sub start {
154 0     0 1 0 my $self = shift;
155 0         0 my $is_class_method = 0;
156              
157 0 0       0 if (!blessed $self) {
158 0         0 $self = $self->new( @_ );
159 0         0 $is_class_method = 1;
160             }
161              
162 0         0 my $bin = $self->bin();
163              
164             # Make sure we have at least Consul 0.6.1 which supports the -dev option.
165 0 0       0 unless ($self->version >= 6_001) {
166 0         0 croak "consul not version 0.6.1 or newer";
167             }
168              
169 0         0 my @opts;
170              
171 0         0 my %config = (
172             node_name => $self->node_name(),
173             datacenter => $self->datacenter(),
174             bind_addr => '127.0.0.1',
175             ports => {
176             dns => -1,
177             http => $self->port() + 0,
178             https => -1,
179             serf_lan => $self->serf_lan_port() + 0,
180             serf_wan => $self->serf_wan_port() + 0,
181             server => $self->server_port() + 0,
182             },
183             );
184              
185             # Version 0.7.0 reduced default performance behaviors in a way
186             # that makese these tests slower to startup. Override this and
187             # make leadership election happen ASAP.
188 0 0       0 if ($self->version >= 7_000) {
189 0         0 $config{performance} = { raft_multiplier => 1 };
190             }
191              
192             # gRPC health checks were added 1.0.5, and in dev mode are enabled and bind
193             # to port 8502, which then clashes if you want to run up a second
194             # Test::Consul. Just disable it.
195 0 0       0 if ($self->version >= 1_000_005) {
196 0         0 $config{ports}{grpc} = -1;
197             }
198              
199 0 0       0 if ($self->enable_acls()) {
200 0 0       0 if ($self->version >= 1_004_000) {
201 0         0 croak "ACLs not supported with Consul >= 1.4.0"
202             }
203              
204 0         0 $config{acl_master_token} = $self->acl_master_token();
205 0         0 $config{acl_default_policy} = $self->acl_default_policy();
206 0         0 $config{acl_datacenter} = $self->datacenter();
207 0         0 $config{acl_token} = $self->acl_master_token();
208             }
209              
210 0 0       0 if (defined $self->enable_remote_exec) {
211 0 0       0 $config{disable_remote_exec} = $self->enable_remote_exec ? JSON->false : JSON->true;
212             }
213              
214 0         0 my $configpath;
215 0 0       0 if ($self->has_datadir()) {
216 0         0 $config{data_dir} = $self->datadir();
217 0         0 $config{bootstrap} = \1;
218 0         0 $config{server} = \1;
219              
220 0         0 my $datapath = path($self->datadir());
221 0         0 $datapath->remove_tree;
222 0         0 $datapath->mkpath;
223              
224 0         0 $configpath = $datapath->child("consul.json");
225             }
226             else {
227 0         0 push @opts, '-dev';
228 0         0 $configpath = path( ( tempfile(SUFFIX => '.json') )[1] );
229             }
230              
231 0         0 $configpath->spew( encode_json(\%config) );
232 0         0 push @opts, '-config-file', "$configpath";
233              
234 0         0 my $pid = fork();
235 0 0       0 unless (defined $pid) {
236 0         0 croak "fork failed: $!";
237             }
238 0 0       0 unless ($pid) {
239 0         0 exec $bin, "agent", @opts;
240             }
241              
242 0         0 my $http = HTTP::Tiny->new(timeout => 10);
243 0         0 my $now = time;
244 0         0 my $res;
245 0         0 my $port = $self->port();
246 0         0 while (time < $now+30) {
247 0         0 $res = $http->get("http://127.0.0.1:$port/v1/status/leader");
248 0 0 0     0 last if $res->{success} && $res->{content} =~ m/^"[0-9\.]+:[0-9]+"$/;
249 0         0 sleep 1;
250             }
251 0 0       0 unless ($res->{success}) {
252 0         0 kill 'KILL', $pid;
253 0         0 croak "consul API test failed: $res->{status} $res->{reason}";
254             }
255              
256 0 0       0 unlink $configpath if !$self->has_datadir();
257              
258 0         0 $self->_pid( $pid );
259              
260 0 0       0 return $self if $is_class_method;
261 0         0 return;
262             }
263              
264             sub stop {
265 0     0 1 0 my ($self) = @_;
266 0 0       0 return unless $self->_has_pid();
267 0         0 my $pid = $self->_pid();
268 0         0 $self->_clear_pid();
269 0         0 kill 'TERM', $pid;
270 0         0 my $now = time;
271 0         0 while (time < $now+2) {
272 0 0       0 return if waitpid($pid, WNOHANG) > 0;
273             }
274 0         0 kill 'KILL', $pid;
275 0         0 return;
276             }
277              
278             sub end {
279 0     0 0 0 goto \&stop;
280             }
281              
282             sub DESTROY {
283 0     0   0 goto \&stop;
284             }
285              
286             sub wan_join {
287 0     0 1 0 my ($self, $other) = @_;
288              
289 0         0 my $http = HTTP::Tiny->new(timeout => 10);
290 0         0 my $port = $self->port;
291 0         0 my $other_wan_port = $other->serf_wan_port;
292              
293 0         0 my $res = $http->put("http://127.0.0.1:$port/v1/agent/join/127.0.0.1:$other_wan_port?wan=1");
294 0 0       0 unless ($res->{success}) {
295 0         0 croak "WAN join failed: $res->{status} $res->{reason}"
296             }
297             }
298              
299             sub found_bin {
300 3     3 1 7 state ($bin, $bin_searched_for);
301 3 50       10 return $bin if $bin_searched_for;
302 3   33     31 my $binpath = $ENV{CONSUL_BIN} || which "consul";
303 3 50 33     897 $bin = $binpath if defined($binpath) && -x $binpath;
304 3         40 $bin_searched_for = 1;
305 3         12 return $bin;
306             }
307              
308             sub skip_all_if_no_bin {
309 3     3 1 301 my ($class) = @_;
310              
311 3 50       36 croak 'The skip_all_if_no_bin method may only be used if the plan ' .
312             'function is callable on the main package (which Test::More ' .
313             'and Test2::Tools::Basic provide)'
314             if !main->can('plan');
315              
316 3 50       12 return if defined $class->found_bin();
317              
318 3         11 main::plan( skip_all => 'The Consul binary must be available to run this test.' );
319             }
320              
321             sub found_version {
322 0     0 1   state ($version);
323 0 0         return $version if defined $version;
324 0           my $bin = found_bin();
325 0           ($version) = qx{$bin version};
326 0 0 0       if ($version and $version =~ m{v(\d+)\.(\d+)\.(\d+)}) {
327 0           $version = sprintf('%03d%03d%03d', $1, $2, $3);
328             }
329             else {
330 0           $version = 0;
331             }
332             }
333              
334             sub skip_all_unless_version {
335 0     0 1   my ($class, $minver, $maxver) = @_;
336              
337 0 0         croak 'usage: Test::Consul->skip_all_unless_version($minver, [$maxver])'
338             unless defined $minver;
339              
340 0 0         croak 'The skip_all_unless_version method may only be used if the plan ' .
341             'function is callable on the main package (which Test::More ' .
342             'and Test2::Tools::Basic provide)'
343             if !main->can('plan');
344              
345 0           $class->skip_all_if_no_bin;
346              
347 0           my $version = $class->found_version;
348              
349 0 0         if (defined $maxver) {
350 0 0 0       return if $minver <= $version && $maxver > $version;
351 0           main::plan( skip_all => "Consul must be between version $minver and $maxver to run this test." );
352             }
353             else {
354 0 0         return if $minver <= $version;
355 0           main::plan( skip_all => "Consul must be version $minver or higher to run this test." );
356             }
357             }
358              
359             1;
360              
361             =pod
362              
363             =encoding UTF-8
364              
365             =for markdown [![Build Status](https://secure.travis-ci.org/robn/Test-Consul.png)](http://travis-ci.org/robn/Test-Consul)
366              
367             =head1 NAME
368              
369             Test::Consul - Run a Consul server for testing
370              
371             =head1 SYNOPSIS
372              
373             use Test::Consul;
374            
375             # succeeds or dies
376             my $tc = Test::Consul->start;
377            
378             my $consul_baseurl = "http://127.0.0.1:".$tc->port;
379            
380             # do things with Consul here
381            
382             # kill test server (or let $tc fall out of scope, destructor will clean up)
383             $tc->end;
384              
385             =head1 DESCRIPTION
386              
387             This module starts and stops a standalone Consul instance. It's designed to be
388             used to help test Consul-aware Perl programs.
389              
390             It's assumed that you have Consul 0.6.4 installed somewhere.
391              
392             =head1 ARGUMENTS
393              
394             =head2 port
395              
396             The TCP port for HTTP API endpoint. Consul's default is C<8500>, but
397             this defaults to a random unused port.
398              
399             =head2 serf_lan_port
400              
401             The TCP and UDP port for the Serf LAN. Consul's default is C<8301>, but
402             this defaults to a random unused port.
403              
404             =head2 serf_wan_port
405              
406             The TCP and UDP port for the Serf WAN. Consul's default is C<8302>, but
407             this defaults to a random unused port.
408              
409             =head2 server_port
410              
411             The TCP port for the RPC Server address. Consul's default is C<8300>, but
412             this defaults to a random unused port.
413              
414             =head2 node_name
415              
416             The name of this node. If not provided, one will be generated.
417              
418             =head2 datacenter
419              
420             The name of the datacenter. If not provided, one will be generated.
421              
422             =head2 enable_acls
423              
424             Set this to true to enable ACLs. Note that Consul ACLs changed substantially in
425             Consul 1.4, and L has not yet been updated to support them. If
426             you try to enable them with Consul 1.4+, L will croak. See
427             L for more info.
428              
429             =head2 acl_default_policy
430              
431             Set this to either C or C. The default is C.
432             See L for more
433             information.
434              
435             =head2 acl_master_token
436              
437             If L is true then this token will be used as the master
438             token. By default this will be C<01234567-89AB-CDEF-GHIJ-KLMNOPQRSTUV>.
439              
440             =head2 enable_acls
441              
442             Set this to true to enable remote execution (off by default since Consul 0.8.0)
443              
444             =head2 bin
445              
446             Location of the C binary. If not provided then the binary will
447             be retrieved from L.
448              
449             =head2 datadir
450              
451             Directory for Consul's data store. If not provided, the C<-dev> option is used
452             and no datadir is used.
453              
454             =head1 ATTRIBUTES
455              
456             =head2 running
457              
458             Returns C if L has been called and L has not been called.
459              
460             =head1 METHODS
461              
462             =head2 start
463              
464             # As an object method:
465             my $tc = Test::Consul->new( %args );
466             $tc->start();
467            
468             # As a class method:
469             my $tc = Test::Consul->start( %args );
470              
471             Starts a Consul instance. This method can take a moment to run, because it
472             waits until Consul's HTTP endpoint is available before returning. If it fails
473             for any reason an exception is thrown. In this way you can be sure that Consul
474             is ready for service if this method returns successfully.
475              
476             =head2 stop
477              
478             $tc->stop();
479              
480             Kill the Consul instance. Graceful shutdown is attempted first, and if it
481             doesn't die within a couple of seconds, the process is killed.
482              
483             This method is also called if the instance of this class falls out of scope.
484              
485             =head2 wan_join
486              
487             my $tc1 = Test::Consul->start;
488             my $tc2 = Test::Consul->start;
489             $tc1->wan_join($tc2);
490              
491             Perform a WAN join to another L instance. Use this to test Consul
492             applications that operate across datacenters.
493              
494             =head1 CLASS METHODS
495              
496             See also L which acts as both a class and instance method.
497              
498             =head2 found_bin
499              
500             Return the value of the C env var, if set, or uses L
501             to search the system for an installed binary. Returns C if no consul
502             binary could be found.
503              
504             =head2 skip_all_if_no_bin
505              
506             Test::Consul->skip_all_if_no_bin;
507              
508             This class method issues a C on the main package if the
509             consul binary could not be found (L returns false).
510              
511             =head2 found_version
512              
513             Return the version of the consul binary, by running the binary return by
514             L with the C argument. Returns 0 if the version can't be
515             determined.
516              
517             =head2 skip_all_unless_version
518              
519             Test::Consul->skip_all_unless_version($minver, [$maxver]);
520              
521             This class method issues a C on the main package if the consul binary
522             is not between C<$minver> and C<$maxvar> (exclusive).
523              
524             =head1 SEE ALSO
525              
526             =over 4
527              
528             =item *
529              
530             L - Consul client library. Uses L in its test suite.
531              
532             =back
533              
534             =head1 SUPPORT
535              
536             =head2 Bugs / Feature Requests
537              
538             Please report any bugs or feature requests through the issue tracker
539             at L.
540             You will be notified automatically of any progress on your issue.
541              
542             =head2 Source Code
543              
544             This is open source software. The code repository is available for
545             public review and contribution under the terms of the license.
546              
547             L
548              
549             git clone https://github.com/robn/Consul-Test.git
550              
551             =head1 AUTHORS
552              
553             =over 4
554              
555             =item *
556              
557             Rob N ★
558              
559             =back
560              
561             =head1 CONTRIBUTORS
562              
563             =over 4
564              
565             =item *
566              
567             Aran Deltac
568              
569             =back
570              
571             =head1 COPYRIGHT AND LICENSE
572              
573             This software is copyright (c) (c) 2015 by Rob N ★ and was supported by FastMail
574             Pty Ltd.
575              
576             This is free software; you can redistribute it and/or modify it under
577             the same terms as the Perl 5 programming language system itself.
578              
579             =cut