File Coverage

blib/lib/Test/Class/Moose/AttributeRegistry.pm
Criterion Covered Total %
statement 86 100 86.0
branch 34 52 65.3
condition 3 6 50.0
subroutine 14 16 87.5
pod 7 7 100.0
total 144 181 79.5


line stmt bran cond sub pod time code
1             package Test::Class::Moose::AttributeRegistry;
2              
3             ## ABSTRACT: Global registry of tags by class and method.
4              
5 33     33   291 use strict;
  33         80  
  33         1102  
6 33     33   181 use warnings;
  33         91  
  33         867  
7 33     33   208 use namespace::autoclean;
  33         84  
  33         355  
8              
9             our $VERSION = '0.99';
10              
11 33     33   3324 use Carp;
  33         86  
  33         2166  
12 33     33   243 use Class::MOP;
  33         85  
  33         1167  
13 33     33   12131 use List::SomeUtils qw( any uniq );
  33         138067  
  33         46221  
14              
15             my %BY_METHOD = (
16             tags => {}, # {$method}{$test_class}
17             plans => {},
18             );
19              
20             sub add_plan {
21 8     8 1 22 my ( undef, $test_class, $method, $plan ) = @_;
22 8 100       24 if ( defined $plan ) {
23 6         16 $plan =~ s/\D//g;
24 6 50       25 undef $plan unless $plan =~ /\d/; # no_plan
25             }
26 8         172 $BY_METHOD{plans}{$method}{$test_class} = $plan;
27             }
28              
29             sub get_plan {
30 0     0 1 0 my ( undef, $test_class, $method ) = @_;
31 0         0 return $BY_METHOD{plans}{$method}{$test_class};
32             }
33              
34             sub has_test_attribute {
35 4192     4192 1 8425 my ( undef, $test_class, $method ) = @_;
36 4192         17029 return exists $BY_METHOD{plans}{$method}{$test_class};
37             }
38              
39             sub add_tags {
40 25     25 1 89 my ( $class, $test_class, $method, $tags ) = @_;
41              
42 25         57 my @tags_copy = @{$tags};
  25         78  
43              
44             # check for additions or deletions to the inherited tag list
45 25 100   29   213 if ( any {/^[-+]/} @tags_copy ) {
  29         132  
46 1         6 @tags_copy = $class->_augment_tags( $test_class, $method, $tags );
47             }
48              
49 25         108 foreach my $tag (@tags_copy) {
50 30 50       170 if ( $tag !~ /^\w+$/ ) {
51 0         0 die "tags must be alphanumeric\n";
52             }
53             }
54              
55             # dedupe tags
56 25         75 my %tags = map { $_ => 1 } @tags_copy;
  30         139  
57              
58 25 50 66     143 if ( exists $BY_METHOD{tags}{$method}
59             && exists $BY_METHOD{tags}{$method}{$test_class} )
60             {
61 0         0 die
62             "tags for $test_class->$method already exists, method redefinition perhaps?\n";
63             }
64              
65 25         83 $BY_METHOD{tags}{$method}{$test_class} = \%tags;
66              
67 25         566 return;
68             }
69              
70             sub tags {
71 0     0 1 0 my @tags;
72 0         0 for my $method ( keys %BY_METHOD ) {
73 0         0 for my $test_class ( keys %{ $BY_METHOD{tags}{$method} } ) {
  0         0  
74 0         0 push @tags, keys %{ $BY_METHOD{tags}{$method}{$test_class} };
  0         0  
75             }
76             }
77              
78 0         0 @tags = sort uniq(@tags);
79 0         0 return @tags;
80             }
81              
82             sub class_has_tag {
83 2     2 1 7 my ( $class, $test_class, $tag ) = @_;
84              
85 2 50       7 croak('no class specified') if not defined $test_class;
86 2 50       6 croak('no tag specified') if not defined $tag;
87              
88             # XXX a naïve implementation, but it does the job for now.
89 2         13 my $test_class_meta = Class::MOP::Class->initialize($test_class);
90 2         38 foreach my $method ( $test_class_meta->get_all_method_names ) {
91 45 100       653 next unless $method =~ /test_/;
92 24 100       48 return 1 if $class->method_has_tag( $test_class, $method, $tag );
93             }
94 1         7 return;
95             }
96              
97             sub method_has_tag {
98 426     426 1 3489 my ( $class, $test_class, $method, $tag ) = @_;
99              
100 426 50       951 croak('no class specified') if not defined $test_class;
101 426 50       791 croak('no method specified') if not defined $method;
102 426 50       829 croak('no tag specified') if not defined $tag;
103              
104             # avoid auto-vivication
105 426 100       1662 return if not exists $BY_METHOD{tags}{$method};
106              
107 103 100       286 if ( not exists $BY_METHOD{tags}{$method}{$test_class} ) {
108              
109             # If this method has no tag data at all, then inherit the tags from
110             # from the superclass
111 4         14 $BY_METHOD{tags}{$method}{$test_class}
112             = $class->_superclass_tags( $test_class, $method );
113             }
114              
115 103         435 return exists $BY_METHOD{tags}{$method}{$test_class}{$tag};
116             }
117              
118             sub _superclass_tags {
119 5     5   16 my ( $class, $test_class, $method ) = @_;
120              
121 5 50       14 croak('no class specified') if not defined $test_class;
122 5 50       16 croak('no method specified') if not defined $method;
123              
124 5 50       16 return {} if not exists $BY_METHOD{tags}{$method};
125              
126 5         24 my $test_class_meta = Class::MOP::Class->initialize($test_class);
127 5         99 my $method_meta;
128              
129 5 50       41 $method_meta = $test_class_meta->find_next_method_by_name($method)
130             if $test_class_meta->can('find_next_method_by_name');
131              
132 5 100       536 if ( !$method_meta ) {
133              
134             #Might be a from a role or this class
135 1         7 my $mm = $test_class_meta->find_method_by_name($method);
136 1         76 my $orig = $mm->original_method;
137              
138 1 50 33     7 if ( $orig && ( $mm->package_name ne $orig->package_name ) ) {
139 1         15 $method_meta = $orig;
140             }
141             }
142              
143             # no method, so no tags to inherit
144 5 50       28 return {} if not $method_meta;
145              
146 5         46 my $super_test_class = $method_meta->package_name();
147 5 50       19 if ( exists $BY_METHOD{tags}{$method}{$super_test_class} ) {
148              
149             # shallow copy the superclass method's tags, because it's possible to
150             # change add/remove items from the subclass's list later
151 7         28 my %tags = map { $_ => 1 }
152 5         10 keys %{ $BY_METHOD{tags}{$method}{$super_test_class} };
  5         27  
153 5         22 return \%tags;
154             }
155              
156             # nothing defined at this level, recurse
157 0         0 return $class->_superclass_tags( $super_test_class, $method );
158             }
159              
160             sub _augment_tags {
161 1     1   4 my ( $class, $test_class, $method, $tags ) = @_;
162              
163 1 50       4 croak('no class specified') if not defined $test_class;
164 1 50       4 croak('no method specified') if not defined $method;
165              
166             # Get the base list from the superclass
167 1         5 my $tag_list = $class->_superclass_tags( $test_class, $method );
168              
169 1         3 for my $tag_definition ( @{$tags} ) {
  1         3  
170 2         7 my $direction = substr( $tag_definition, 0, 1 );
171 2         5 my $tag = substr( $tag_definition, 1 );
172 2 100       8 if ( $direction eq '+' ) {
    50          
173 1         5 $tag_list->{$tag} = 1;
174             }
175             elsif ( $direction eq '-' ) {
176              
177             # die here if the tag wasn't inherited?
178 1         4 delete $tag_list->{$tag};
179             }
180             else {
181 0         0 die
182             "$test_class->$method attempting to override and modify tags, did you forget a '+'?\n";
183             }
184             }
185              
186 1         2 return keys %{$tag_list};
  1         15  
187             }
188              
189             1;
190              
191             __END__
192              
193             =pod
194              
195             =encoding UTF-8
196              
197             =head1 NAME
198              
199             Test::Class::Moose::AttributeRegistry - Global registry of tags by class and method.
200              
201             =head1 VERSION
202              
203             version 0.99
204              
205             =head1 SYNOPSIS
206              
207             use Test::Class::Moose::AttributeRegistry;
208              
209             my @tags = Test::Class::Moose::AttributeRegistry->tags;
210             print Test::Class::Moose::AttributeRegistry->method_has_tag( 'TestsFor::FooBar', 'test_baz', 'network' );
211              
212             =head1 DESCRIPTION
213              
214             This class permits addition and querying of the tags and plans defined on
215             methods via attributes. It's been gleefully stolen from
216             L<Attribute::Method::Tags> and is for internal use only. Don't rely on this
217             code.
218              
219             =head1 METHODS
220              
221             All the following are class methods, as the attribute registry is shared
222             globally. Note that all parameters for any of the methods below are required.
223              
224             =over 4
225              
226             =item add_plan( $class, $method, plan )
227              
228             Add a numeric (or undef) plan to a method.
229              
230             =item get_plan( $class, $method )
231              
232             Returns the numeric (or undef) plan for a method if that was set via the
233             C<Test> or C<Tests> attributes.
234              
235             =item has_test_attribute( $class, $method )
236              
237             Returns true if either C<Test> or C<Tests> was declared for a method. Used to
238             identify something as a test method even if the method name doesn't begin with
239             C<test_>.
240              
241             =item add_tags( $class, $method, $tags_ref )
242              
243             Adds the given list of tags (as an array-ref) for the specified class/method
244             combination. An exception will be raised if either the tags are
245             non-alphanumeric or the method is one that has already had tags registered for
246             it.
247              
248             =item tags
249              
250             Find all tags defined for all methods. Returns a sorted list of tags.
251              
252             =item class_has_tag( $class, $tag )
253              
254             Returns a boolean (0|1) indicating whether the given class has any method with
255             the specified tag.
256              
257             =item method_has_tag( $class, $method, $tag )
258              
259             Returns a boolean (0|1) indicating whether the given method in the given class
260             has the specified tag.
261              
262             =back
263              
264             =head1 SEE ALSO
265              
266             =over 4
267              
268             =item L<Attribute::Method::Tags>
269              
270             Attribute-based interface for adding tags to methods. Your author "liberated"
271             this code from L<Attribute::Method::Tags::Registry> (with a tip 'o the keyboard
272             to Mark Morgan for his work on this).
273              
274             =back
275              
276             =head1 SUPPORT
277              
278             Bugs may be submitted at L<https://github.com/houseabsolute/test-class-moose/issues>.
279              
280             I am also usually active on IRC as 'autarch' on C<irc://irc.perl.org>.
281              
282             =head1 SOURCE
283              
284             The source code repository for Test-Class-Moose can be found at L<https://github.com/houseabsolute/test-class-moose>.
285              
286             =head1 AUTHORS
287              
288             =over 4
289              
290             =item *
291              
292             Curtis "Ovid" Poe <ovid@cpan.org>
293              
294             =item *
295              
296             Dave Rolsky <autarch@urth.org>
297              
298             =back
299              
300             =head1 COPYRIGHT AND LICENSE
301              
302             This software is copyright (c) 2012 - 2021 by Curtis "Ovid" Poe.
303              
304             This is free software; you can redistribute it and/or modify it under
305             the same terms as the Perl 5 programming language system itself.
306              
307             The full text of the license can be found in the
308             F<LICENSE> file included with this distribution.
309              
310             =cut