File Coverage

blib/lib/Sport/Analytics/NHL/Test.pm
Criterion Covered Total %
statement 557 611 91.1
branch 199 278 71.5
condition 201 321 62.6
subroutine 85 85 100.0
pod 60 60 100.0
total 1102 1355 81.3


line stmt bran cond sub pod time code
1             package Sport::Analytics::NHL::Test;
2              
3 49     49   26389 use v5.10.1;
  49         177  
4 49     49   261 use strict;
  49         104  
  49         1360  
5 49     49   357 use warnings FATAL => 'all';
  49         104  
  49         2019  
6 49     49   284 use experimental qw(smartmatch);
  49         103  
  49         410  
7              
8 49     49   3167 use parent 'Exporter';
  49         177  
  49         344  
9              
10 49     49   3057 use Carp;
  49         236  
  49         2693  
11 49     49   5422 use Data::Dumper;
  49         56161  
  49         2302  
12 49     49   8126 use Storable;
  49         42455  
  49         2494  
13              
14 49     49   4690 use List::MoreUtils qw(uniq);
  49         100402  
  49         486  
15              
16 49     49   34211 use Sport::Analytics::NHL::Config;
  49         106  
  49         9857  
17 49     49   341 use Sport::Analytics::NHL::LocalConfig;
  49         129  
  49         6065  
18 49     49   4137 use Sport::Analytics::NHL::Util;
  49         113  
  49         3329  
19 49     49   4352 use Sport::Analytics::NHL::Tools;
  49         112  
  49         8836  
20 49     49   10076 use Sport::Analytics::NHL::Errors;
  49         291  
  49         30612  
21              
22             =head1 NAME
23              
24             Sport::Analytics::NHL::Test - Utilities to test NHL reports data.
25              
26             =head1 SYNOPSYS
27              
28             Utilities to test NHL report data
29              
30             These are utilities that test and validate the data contained in the NHL reports to detect errors. They are also used to test and validate the permutations that are performed by this software on the data.
31             Ideally, that method should extend Test::More, but first, I was too lazy to figure out how to do it, and second, I notice that in the huge number of tests that are run, Test::More begins to drag things down.
32              
33             use Sport::Analytics::NHL::Test;
34             test_team_id('SJS') # pass
35             test_team_id('S.J') # fail and die (usually)
36              
37             The failures are usually bad enough to force the death of the program and an update to Sport::Analytics::NHL::Errors (q.v.), but see the next section
38              
39             =head1 GLOBAL VARIABLES
40              
41             The behaviour of the tests is controlled by several global variables:
42             * $TEST_COUNTER - contains the number of the current test in Curr_Test field and the number of passes/fails in Test_Results.
43             * $DO_NOT_DIE - when set to 1, failed test will not die.
44             * $MESSAGE - the latest failure message
45             * $TEST_ERRORS - accumulation of errors by type (event, player, boxscore, team)
46              
47             =head1 FUNCTIONS
48              
49             =over 2
50              
51             =item C
52              
53             Either dies with a stack trace dump, or aggregates the error messages, based on $DO_NOT_DIE
54             Arguments: the death message
55             Returns: void
56              
57             =item C
58              
59             Executes a test subroutine and sets the failure message in case of failure. Updates test counters.
60             Arguments: the test subroutine and its arguments
61             Returns: void
62              
63             =item C
64              
65             Approximately the same as Test::More::like()
66              
67             =item C
68              
69             Approximately the same as Test::More::is()
70              
71             =item C
72              
73             Approximately the same as Test::More::ok()
74              
75             =item C
76              
77             Approximately the same as grep {$_[0] == $_} $_[1]
78              
79             =item C
80              
81             For the test_* functions below the second argument is always the notification message. Sometimes third parameter may be passed. This one tests if the season is one between $FIRST_SEASON (from Sports::Analytics::NHL::Config) and $CURRENT_SEASON (from Sports::Analytics::NHL::LocalConfig)
82              
83             =item C
84              
85             Tests if the stage is either Regular (2) or Playoff (3)
86              
87             =item C
88              
89             Tests the season Id to be between 1 and 1500 (supposedly maximum number of games per reg. season)
90              
91             =item C
92              
93             Tests the game Id to be of the SSSSTIIII form. In case optional parameter is_nhl, tests for the NHL id SSSSTTIIII
94              
95             =item C
96              
97             Tests if the string is a three-letter team code, not necessarily the normalized one.
98              
99             =item C
100              
101             Tests if the string is a three-letter franchise code, as specified in keys of Sports::Analytics::NHL::Config::TEAMS
102              
103             =item C
104              
105             Tests the timestamp to be an integer (negative for pre-1970 games) number.
106              
107             =item C
108              
109             Tests the game date to be in YYYYMMDD format.
110              
111             =item C
112              
113             Check if the particular stat is measured in season being processed, stored in $THIS_SEASON
114              
115             =item C
116              
117             Set the stats tested for a player
118              
119             =item C
120              
121             Tests the correct values in assists and servedby fields
122              
123             =item C
124              
125             Overall sequence to test the entire boxscore
126              
127             =item C
128              
129             Tests the event coordinates
130              
131             =item C
132              
133             Tests the decision for the goaltender being one of W,L,O,T
134              
135             =item C
136              
137             Overall sequence to test the event
138              
139             =item C
140              
141             Route the particular event testing according to its type
142              
143             =item C
144              
145             Checks the applicability of coordinates for the event and tests them
146              
147             =item C
148              
149             Tests the event's description (for existence, actually)
150              
151             =item C
152              
153             Checks the applicability of strength setting for the event and tests it
154              
155             =item C
156              
157             Test the boxscore's events (loops over test_event (q.v.))
158              
159             =item C
160              
161             Tests the goal event
162              
163             =item C
164              
165             Tests the header information of the boxscore
166              
167             =item C
168              
169             Tests the player's name (to have a space between two words, more or less)
170              
171             =item C
172              
173             Tests the officials definition in the boxscore
174              
175             =item C
176              
177             Tests the penalty event
178              
179             =item C
180              
181             Tests the periods' reports from the boxscore
182              
183             =item C
184              
185             Tests a player entry in the team's roster
186              
187             =item C
188              
189             Tests valid population of the player1 event field
190              
191             =item C
192              
193             Tests valid population of the player2 event field
194              
195             =item C
196              
197             Tests the player id to be the valid 7-digit one one starting with 8
198              
199             =item C
200              
201             Tests the position of the player to be one of C L R F D G
202              
203             =item C
204              
205             Tests the event's strength to be one of EV, SH, PP, PS or XX (unknown)
206              
207             =item C
208              
209             Tests "header" information for a team: shots, score, coach etc.
210              
211             =item C
212              
213             Tests teams that played in the game
214              
215             =item C
216              
217             Tests the time to be of format M{1,3}:SS
218              
219             =item C
220              
221             Tests the boxscore after it was merged with other files (e.g. via Sport::Analytics::NHL::Merger). Test options are set according to the types of reports that have been merged ($boxscore->{sources})
222              
223             =item C
224              
225             Tests the events of the merged boxscore
226              
227             =item C
228              
229             Tests the header of the merged boxscore
230              
231             =item C
232              
233             Tests the teams of the merged boxscore
234              
235             =item C
236              
237             Approximately the same as Test::More::cmp_ok()
238              
239             =item C
240              
241             Tests that the normalized events have been arranged correctly
242              
243             =item C
244              
245             Tests the consistency between the event summary and the boxscore
246              
247             =item C
248              
249             Tests the consistency between the goaltender data and the event summary
250              
251             =item C
252              
253             Tests the consistency between the penalty minutes of the players in the boxscore and in the event summary
254              
255             =item C
256              
257             Tests the consistency between the goals scored in the events and the goals in the boxscore teams/players
258              
259             =item C
260              
261             Tests the consistency between the skater data and the event summary
262              
263             =item C
264              
265             Full test of the normalized boxscore
266              
267             =item C
268              
269             Tests the normalized boxscore events
270              
271             =item C
272              
273             Tests the normalized boxscore's header
274              
275             =item C
276              
277             Tests the normalized roster of a team
278              
279             =item C
280              
281             Tests the normalized boxscore's teams
282              
283             =item C
284              
285             Tests the player's bio and draft data retrieved from the NHL website
286              
287             =item C
288              
289             Tests the player's career data retrieved from the NHL website and amended with the preserved errata.
290              
291             =item C
292              
293             Calls test_bio() and test_career() to test the player report from the NHL website. Executes presence, syntactic and sanity value checks.
294              
295             =back
296              
297             =cut
298              
299             our $TEST_COUNTER = {Curr_Test => 0, Test_Results => []};
300              
301             our @EXPORT = qw(
302             my_like my_ok my_is
303             test_game_id test_team_id test_team_code
304             test_stage test_season test_season_id
305             test_ts test_game_date
306             test_header test_periods test_officials test_teams test_events
307             test_boxscore test_merged_boxscore
308             test_consistency test_normalized_boxscore
309             test_player_report
310             $TEST_COUNTER
311             $EVENT $BOXSCORE $PLAYER $TEAM
312             );
313              
314             our $DO_NOT_DIE = 0;
315             our $TEST_ERRORS = {};
316             our $MESSAGE = '';
317             our $THIS_SEASON;
318              
319             our $EVENT;
320             our $BOXSCORE;
321             our $PLAYER;
322              
323             $Data::Dumper::Trailingcomma = 1;
324             $Data::Dumper::Deepcopy = 1;
325             $Data::Dumper::Sortkeys = 1;
326             $Data::Dumper::Deparse = 1;
327              
328             sub my_die ($) {
329              
330 68     68 1 83 my $message = shift;
331 68 50       103 if ($DO_NOT_DIE) {
332 68         86 my $field;
333             my $object;
334 68 50       105 if ($EVENT) {
    50          
335 0         0 $field = 'events';
336 0         0 $object = $EVENT;
337             }
338             elsif ($PLAYER) {
339 0         0 $field = 'players';
340 0         0 $object = $PLAYER;
341             }
342             else {
343 68         74 $field = 'boxscore';
344 68         75 $object = $BOXSCORE;
345             }
346 68   50     245 $TEST_ERRORS->{$field} ||= [];
347             push(
348 68         305 @{$TEST_ERRORS->{$field}},
349             {
350             _id => $object->{_id} || $object->{event_idx} || $object->{number},
351 68   33     83 message => $MESSAGE,
352             }
353             );
354 68         123 return;
355             }
356 0 0       0 $message .= "\n" unless $message =~ /\n$/;
357 0         0 my $c = 0;
358 0         0 my $offset = '';
359 0         0 while (my @caller = caller($c++)) {
360 0         0 $message .= sprintf(
361             "%sCalled in %s::%s, line %d in %s\n",
362             $offset, $caller[0], $caller[3], $caller[2], $caller[1]
363             );
364 0         0 $offset .= ' ';
365             }
366 0         0 die $message;
367             }
368              
369             sub my_test ($@) {
370              
371 226047     226047 1 275911 my $test = shift;
372 226047         262348 $TEST_COUNTER->{Curr_Test}++;
373 49     49   384 no warnings 'uninitialized';
  49         113  
  49         10085  
374 226047 100       319129 if (@_ == 2) {
375 150883         239603 $MESSAGE = "Failed $_[-1]: $_[0]";
376             }
377             else {
378 75164 100 100     199095 if (ref $_[1] && ref $_[1] eq 'ARRAY') {
379 74         76 my $arg1 = join('/', @{$_[1]});
  74         137  
380 74         121 $MESSAGE = "Failed $_[-1]: $_[0] vs $arg1\n";
381             }
382             else {
383 75090         159081 $MESSAGE = "Failed $_[-1]: $_[0] vs $_[1]\n";
384             }
385             }
386 226047 100       321970 if ($test->(@_)) {
387 225979         290437 $TEST_COUNTER->{Test_Results}[0]++;
388             }
389             else {
390 68         80 $TEST_COUNTER->{Test_Results}[1]++;
391 68         100 my_die($MESSAGE);
392             }
393 49     49   321 use warnings FATAL => 'all';
  49         111  
  49         6404  
394 226047 50 33     514222 debug "ok_$TEST_COUNTER->{Curr_Test} - $_[-1]" if $IS_AUTHOR && $0 =~ /\.t$/;
395             }
396              
397 49     49 1 958 sub my_like ($$$) { my_test(sub { no warnings 'uninitialized'; $_[0] =~ $_[1] }, @_) }
  49     72138   431  
  49     72138   3922  
  72138         294664  
  72138         181878  
398 49     49 1 323 sub my_is ($$$) { my_test(sub { no warnings 'uninitialized'; $_[0] eq $_[1] }, @_) }
  49     2952   92  
  49     2952   3729  
  2952         5586  
  2952         8265  
399 49     49 1 292 sub my_ok ($$) { my_test(sub { no warnings 'uninitialized'; $_[0] }, @_) }
  49     150883   134  
  49     150883   3340  
  150883         238391  
  150883         351875  
400 49     49 1 286 sub my_is_one_of ($$$) { my_test(sub { no warnings 'uninitialized'; grep { $_[0] == $_ } @{$_[1]}}, @_) }
  49     74   115  
  49     74   426651  
  74         63  
  222         311  
  74         95  
  74         208  
401              
402             sub my_cmp_ok ($$$$) {
403 1539     1539 1 2761 my ($got, $type, $expect, $message) = @_;
404 1539         1582 my $test;
405 1539         55797 eval qq{
406             \$test = (\$got $type \$expect);
407             1;
408             };
409 1539 50       3941 my_die($@) if $@;
410 1539         2624 my_ok($test, $message);
411             }
412              
413              
414             sub test_season ($$) {
415 23519     23519 1 50975 my $season = shift;
416 23519         27799 my $message = shift;
417 23519         51342 my_ok($season >= $FIRST_SEASON, $message); my_ok($season <= $CURRENT_SEASON, $message);
  23519         60534  
418 23519         47234 $THIS_SEASON = $season;
419             }
420              
421             sub test_stage ($$) {
422 23519     23519 1 48703 my $stage = shift;
423 23519         28043 my $message = shift;
424 23519         45413 my_ok($stage >= $REGULAR, 'stage ok'); my_ok($stage <= $PLAYOFF, $message);
  23519         47468  
425             }
426              
427             sub test_season_id ($$) {
428 23519     23519 1 44393 my $id = shift;
429 23519         27732 my $message = shift;
430 23519         46537 my_ok($id > 0, $message); my_ok($id < 1500, $message);
  23519         47095  
431             }
432              
433             sub test_game_id ($$;$) {
434 17702     17702 1 1233436 my $id = shift;
435 17702         22066 my $message = shift;
436 17702   100     34434 my $is_nhl = shift || 0;
437              
438 17702 100       54941 $is_nhl
439             ? $id =~ /^(\d{4})(\d{2})(\d{4})$/
440             : $id =~ /^(\d{4})(\d{1})(\d{4})$/;
441 17702         37277 test_season($1, $message);
442 17702         33732 test_stage($2, $message);
443 17702         36156 test_season_id($3, $message);
444             }
445              
446             sub test_team_code ($$) {
447 17857     17857 1 69727 my_like(shift, qr/^\w{3}$/, shift .' tri letter code a team');
448             }
449              
450 12433 50   12433 1 50611 sub test_team_id ($$) { test_team_code($_[0],$_[1]) && my_ok($TEAMS{$_[0]}, "$_[0] team defined")};
451 5807     5807 1 32543 sub test_ts ($$) { my_like(shift, qr/^-?\d+$/, shift) }
452 5807     5807 1 28030 sub test_game_date ($$) { my_like(shift, qr/^\d{8}$/, shift) }
453              
454             sub is_unapplicable ($) {
455 6534     6534 1 8186 my $data = shift;
456              
457             $THIS_SEASON < (
458             $DATA_BY_SEASON{$data} || $STAT_RECORD_FROM{$data} || $data
459 6534 100 66     36789 ) || $EVENT && $EVENT->{time} eq '00:00' && $EVENT->{period} < 2;
      33        
      66        
460             };
461              
462             sub test_header ($) {
463              
464 10     10 1 24 my $bs = shift;
465              
466 10         60 test_season( $bs->{season}, 'header season ok');
467 10         42 test_stage( $bs->{stage}, 'header stage ok');
468 10         46 test_season_id($bs->{season_id}, 'header season id ok');
469 10         49 test_game_id( $bs->{_id}, 'header game id ok');
470              
471 10         51 my_is($bs->{status}, 'FINAL', 'only final games');
472 10 100       57 my_ok($bs->{location}, 'location set') unless is_unapplicable('location');
473              
474             my_like($bs->{ot}, qr/^0|1$/, 'OT detected')
475 10 50       22 if @{$bs->{periods}} > 3;
  10         45  
476             my_like($bs->{so}, qr/^0|1$/, 'SO detected')
477 10 50 33     19 if @{$bs->{periods}} > 4 && $bs->{stage} == $REGULAR;
  10         99  
478 10 50 33     54 if ($bs->{so} && ref $bs->{shootout}) {
479 0         0 for my $team (qw(away home)) {
480 0         0 for my $stat (qw(attempts scores)) {
481 0         0 my_like($bs->{shootout}{$team}{$stat}, qr/^\d+$/, 'shootout stat ok');
482             }
483             }
484             }
485             }
486              
487             sub test_officials ($;$) {
488              
489 4     4 1 25 my $officials = shift;
490 4         10 return 1; # for now
491              
492 0         0 for my $o (qw(referees linesmen)) {
493 0         0 for my $of (@{$officials->{$o}}) {
  0         0  
494 0         0 my_ok($of->{name}, 'name set');
495             }
496             }
497             }
498              
499 432     432 1 1301 sub test_name ($$) { my_like(shift, qr/\w|\.\s+\w/, shift.' first and last name') ; }
500 18718     18718 1 66477 sub test_player_id ($$) { my_like(shift, qr/^8\d{6}$/, shift.' valid player id') ; }
501 2712     2712 1 7874 sub test_time ($$) { my_like(shift, qr/^\-?\d{1,3}:\d{1,2}$/, shift.' valid time') ; }
502 413     413 1 1234 sub test_position ($$) { my_like(shift, qr/^(C|R|W|F|D|L|G)$/, shift.' valid pos defined') ; }
503 12     12 1 60 sub test_decision ($$) { my_like(shift, qr/^W|L|O|T|N$/, shift.' valid decision') ; }
504 555     555 1 1587 sub test_strength ($$) { my_like(shift, qr/^EV|SH|PP|PS|XX$/, shift.' valid strength') ; }
505              
506             sub test_periods ($) {
507              
508 5     5 1 186 my $periods = shift;
509              
510 5         21 for my $p (0..4) {
511 25         52 my $period = $periods->[$p];
512 25 100 66     81 next if ! $period && $p > 2;
513 15         53 my_is($period->{id}, $p+1, 'period id ok');
514 15         75 my_like($period->{type}, qr/^REGULAR|OVERTIME$/, 'period time ok');
515 15         37 my_is(scalar(@{$period->{score}}), 4, '4 items in score');
  15         49  
516 15         33 for my $gssg (@{$period->{score}}) {
  15         30  
517 60         132 my_like($gssg, qr/^\d+$/, 'gssg in period a number');
518             }
519             }
520             }
521              
522             sub test_coords ($) {
523              
524 1490     1490 1 2451 my $coords = shift;
525              
526 1490 100       1801 return if scalar keys %{$coords} < 2;
  1490         4217  
527 1235         1782 my_is(scalar(keys %{$coords}), 2, '2 coords');
  1235         3193  
528              
529 1235         2722 for my $coord (keys %{$coords}) {
  1235         2921  
530 2470         7453 my_like($coord, qr/^x|y$/, 'coord x or y');
531 2470         8481 my_like($coords->{$coord}, qr/^\-?\d+$/, 'event coord ok');
532             }
533             }
534              
535             sub test_team_header ($;$) {
536              
537 18     18 1 34 my $team = shift;
538 18   50     47 my $opts = shift || {};
539              
540             test_team_code($team->{name}, 'team name ok')
541 18 100 100     153 unless $opts->{es} || $opts->{gs} || $opts->{ro};
      100        
542             test_name( $team->{coach}, 'team coach ok')
543 18 100 100     128 unless $opts->{es} || $opts->{gs};
544 18 100       73 my_like($team->{shots}, qr/^\d{1,2}$/, 'shots a number') if $opts->{bs};
545 18         90 my_like($team->{score}, qr/^1?\d$/, 'goals < 20');
546 18 100       69 my_like($team->{pull}, qr/^1|0$/, 'goalie either pulled or not') if $opts->{bs};
547 18         37 for my $scratch (@{$team->{scratches}}) {
  18         52  
548             $opts->{ro} ?
549 25 100       71 test_name($scratch->{name}, 'scratch name ok in ro') :
550             test_player_id($scratch, 'scratch id ok');
551             }
552             }
553              
554             sub set_tested_stats ($$) {
555              
556 304     304 1 342 my $player = shift;
557 304   50     448 my $opts = shift || {};
558              
559 304         343 my @stats;
560 304 50       509 return () if $player->{missing};
561 304 100       595 if ($opts->{gs}) {
    100          
    100          
562             @stats = $player->{old} ?
563 4 50       13 qw(timeOnIce shots saves goals) :
564             qw(timeOnIce number powerPlayTimeOnIce shortHandedTimeOnIce evenTimeOnIce shots saves goals);
565             }
566             elsif ($opts->{ro}) {
567 40         65 @stats = qw(number start);
568             }
569             elsif ($opts->{es}) {
570              
571             }
572             else {
573 220 100       902 @stats = $player->{position} eq 'G' ?
574             qw(pim evenShotsAgainst shots timeOnIce shortHandedShotsAgainst assists shortHandedSaves powerPlayShotsAgainst powerPlaySaves evenSaves number saves goals) :
575             qw(penaltyMinutes shortHandedAssists goals evenTimeOnIce takeaways blocked assists hits powerPlayTimeOnIce plusMinus powerPlayGoals giveaways faceoffTaken faceOffWins shortHandedGoals powerPlayAssists number timeOnIce shots shortHandedTimeOnIce);
576 220 100       397 $stats[0] = 'penaltyMinutes' if $opts->{merged};
577             }
578 304         997 @stats;
579             }
580              
581             sub test_player ($;$) {
582              
583 304     304 1 353 my $player = shift;
584 304   50     484 my $opts = shift || {};
585              
586 304         512 my @stats = set_tested_stats($player, $opts);
587 304         645 test_position($player->{position}, 'roster position ok');
588 304         758 for my $stat (@stats) {
589             next if is_unapplicable($STAT_RECORD_FROM{$stat})
590 4428 100 66     8114 || $player->{position} eq 'G' && $opts->{es};
      66        
591 3992 50       7298 if (! defined $player->{$stat}) {print Dumper $stat, $player;exit;}
  0         0  
  0         0  
592             $stat =~ /timeonice/i ?
593             $opts->{es} || $opts->{gs} ?
594             my_like($player->{$stat}, qr/^\d{1,5}$/, "ES $stat ok") :
595             test_time($player->{$stat}, "$stat timeonice ok") :
596 3992 100 66     14778 my_like($player->{$stat}, qr/\-?\d{1,2}/, "stat $stat an integer");
    100          
597             }
598 304         667 test_name($player->{name}, 'player name ok');
599             test_player_id($player->{_id}, 'roster id ok')
600 304 100 100     1447 unless $opts->{es} || $opts->{gs} || $opts->{ro};
      100        
601              
602             }
603              
604             sub test_teams ($;$) {
605              
606 9     9 1 30 my $teams = shift;
607 9   100     34 my $opts = shift || {};
608              
609 9         23 for my $team (@{$teams}) {
  9         27  
610 18         85 test_team_header($team, $opts);
611 18         37 my $decision = '';
612 18         36 my $broken = 0;
613 18         27 for my $player (@{$team->{roster}}) {
  18         47  
614 324 50 66     1041 next if $player->{_id} && $player->{_id} =~ /^80/;
615 324         414 $PLAYER = $player;
616 324 100       573 if ($player->{broken}) {
617 20         34 $broken = 1;
618 20         45 next;
619             }
620 304         575 test_player($player, $opts);
621 304 100       785 if (! $decision) {
    50          
622 197         277 $decision = $player->{decision};
623             }
624             elsif ($player->{decision}) {
625 0         0 die "Cannot have two decisions";
626             }
627 304         467 undef $PLAYER;
628             }
629             test_decision($decision, 'game decision ok')
630             unless $broken
631             || $BOXSCORE->{_gs_no_g}
632             || $opts->{es}
633 18 100 66     406 || $opts->{ro};
      66        
      100        
634 18 100       74 $team->{decision} = $decision if $opts->{merged};
635             }
636 9         33 undef $PLAYER;
637             }
638              
639             sub test_event_strength ($$$) {
640              
641 1952     1952 1 2572 my $event = shift;
642 1952         2194 my $opts = shift;
643 1952         2313 my $message = shift;
644              
645             test_strength($event->{strength}, $message)
646             if $event->{type} eq 'GOAL' || $opts->{merged} && (
647             !$BROKEN_TIMES{$BOXSCORE->{_id}}
648             && $event->{type} ne 'CHL'
649             && !($event->{type} eq 'PENL' && ! $event->{sources}{PL})
650             && ($event->{type} eq 'GOAL' || $BOXSCORE->{sources}{PL}
651             && ! is_noplay_event($event))
652             && !($event->{type} eq 'MISS' && ! $event->{sources}{PL})
653 1952 100 33     10465 );
      100        
      66        
      66        
      100        
      66        
      66        
      100        
      100        
654             }
655              
656             sub test_event_coords ($) {
657 1952     1952 1 2432 my $event = shift;
658              
659             test_coords($event->{coordinates})
660             if !is_unapplicable('coordinates')
661             && !is_noplay_event($event)
662             && !($event->{penalty})
663 1952 100 66     3067 && !($BROKEN_COORDS{$BOXSCORE->{_id}});
664             }
665              
666             sub test_event_description ($) {
667 1952     1952 1 2839 my $event = shift;
668              
669             my_like($event->{description}, qr/\w/, 'event description exists')
670             if $BOXSCORE->{sources}{BS}
671             && !$BROKEN_FILES{$BOXSCORE->{_id}}->{BS}
672 1952 100 66     7940 || $BOXSCORE->{sources}{PL};
      100        
673             }
674              
675             sub test_assists_and_servedby ($$) {
676 1952     1952 1 2466 my $event = shift;
677 1952   50     3245 my $opts = shift || {};
678              
679 1952 100       3733 if ($event->{servedby}) {
680             $opts->{pl} ?
681             my_like($event->{player1}, qr/^(\d{1,2}|80\d{5})$/, 'pl player1 number ok') :
682 7 100       46 test_player_id($event->{servedby}, 'servedby player id ok');
683             }
684 1952 100 100     3887 if ($event->{assists} && @{$event->{assists}}) {
  34         116  
685 33         57 for my $assist (@{$event->{assists}}) {
  33         89  
686             $opts->{pl} ?
687 66 100       230 my_like($event->{player1}, qr/^(\d{1,2}|80\d{5})$/, 'pl assist number ok') :
688             test_player_id($assist, 'assist id ok');
689             }
690             }
691             }
692              
693             sub test_player1 ($$) {
694 1619     1619 1 2072 my $event = shift;
695 1619         1929 my $opts = shift;
696              
697 1619 100 66     5681 if (($opts->{gs} && ! $event->{old}) || $opts->{pl}) {
      100        
698 268         887 my_like($event->{player1}, qr/^(\d{1,2}|80\d{5})$/, 'gs pl player1 number ok');
699             }
700             else {
701 1351         1787 $DO_NOT_DIE = 1;
702             test_player_id($event->{player1}, 'event player1 ok')
703             unless $opts->{gs}
704             || ($event->{type} eq 'PENL'
705             && ($event->{time} eq '20:00'
706             || $PENALTY_POSSIBLE_NO_OFFENDER{$event->{penalty}})
707 1351 100 66     5509 );
      100        
      66        
708 1351         3568 $DO_NOT_DIE = 0;
709             }
710             }
711              
712             sub test_player2 ($$) {
713 807     807 1 1104 my $event = shift;
714 807         962 my $opts = shift;
715              
716             test_player_id($event->{player2}, 'event player2 ok')
717             unless ($event->{type} eq 'GOAL' && $event->{en})
718             || ($event->{type} eq 'GOAL' && $opts->{bh} || $opts->{gs} || $opts->{pl})
719             || ($opts->{merged} && ! $event->{sources}{BS} && $event->{type} eq 'GOAL')
720 807 50 66     7606 || ($event->{time} eq '0:00' && $event->{type} ne 'FAC');
      100        
      100        
      100        
      66        
      66        
      33        
      66        
      66        
      33        
721             }
722              
723             sub test_goal ($$) {
724 39     39 1 63 my $event = shift;
725 39         59 my $opts = shift;
726              
727 39 50 66     361 unless (
      66        
      33        
      33        
728             $opts->{pb} || $opts->{pl} || $event->{so}
729             || $BROKEN_FILES{BS}->{$BOXSCORE->{_id}} && $BROKEN_FILES{BS}->{$BOXSCORE->{_id}} == $NO_EVENTS
730             ) {
731 34 100 66     183 my_like($event->{en}, qr/^0|1$/, 'en definition') if $event->{sources}{BS} || $event->{sources}{GS};
732             my_like($event->{gwg}, qr/^0|1$/, 'gwg definition')
733 34 100       119 if $opts->{bs};
734             }
735             }
736              
737             sub test_penalty ($$) {
738 128     128 1 182 my $event = shift;
739 128         165 my $opts = shift;
740 128 50       284 unless ($opts->{pb}) {
741             my_like(
742             $event->{severity},
743             qr/^major|misconduct|minor|game|match|double|shot$/i, 'severity defined'
744             ) unless ! defined $event->{severity} || is_unapplicable('severity')
745             || $opts->{bh}
746             || $opts->{gs}
747             || $opts->{pl}
748             || !$event->{length}
749 128 50 100     403 || $BROKEN_FILES{BS}->{$BOXSCORE->{_id}} && $BROKEN_FILES{BS}->{$BOXSCORE->{_id}} == $NO_EVENTS;
      66        
      33        
      33        
      33        
      33        
      33        
750 128         637 my_ok($VOCABULARY{penalty}->{$event->{penalty}}, "$event->{penalty} Good penalty type");
751 128         454 my_like($event->{length}, qr/^0|2|3|4|5|10$/, 'length defined');
752             }
753             }
754              
755             sub test_event_by_type ($$) {
756 1952     1952 1 2426 my $event = shift;
757 1952         2410 my $opts = shift;
758              
759 1952         5534 my_ok($VOCABULARY{events}->{$event->{type}}, "$event->{type} Good event type");
760             my_ok($VOCABULARY{strength}->{$event->{strength}}, 'Good event strength')
761 1952 50       7269 if exists $event->{strength};
762 1952         4479 for ($event->{type}) {
763 1952         6842 when ([ qw(FAC HIT BLOCK GOAL SHOT PENL MISS GIVE TAKE) ]) {
764 1619         3522 test_player1($event, $opts);
765 1619         2569 continue;
766             }
767 1952         5588 when ([ qw(FAC HIT BLOCK GOAL) ]) {
768 807         1850 test_player2($event, $opts);
769 807         1909 continue;
770             }
771 1952         3781 when ('STOP') {
772 294         906 my_is(ref $event->{stopreason}, 'ARRAY', 'stopreason is array');
773 294         622 for my $reason (@{$event->{stopreason}}) {
  294         638  
774             my_ok(
775 322         879 $VOCABULARY{stopreason}->{$reason},
776             "$reason there is a good reason to stop",
777             );
778             }
779 294         452 continue;
780             }
781 1952         3826 when ([ qw(GOAL SHOT) ]) {
782             my_ok(
783             $VOCABULARY{shot_type}->{$event->{shot_type}},
784 417         1481 "$event->{shot_type} shot type normalized",
785             );
786 417         838 continue;
787             }
788 1952         3606 when ([ qw(GOAL) ]) {
789 39         123 test_goal($event, $opts);
790 39         99 continue;
791             }
792 1952         3377 when ([ qw(MISS) ]) {
793             my_ok(
794             $VOCABULARY{miss}->{$event->{miss}},
795 192         601 'miss type normalized',
796             );
797             my_like($event->{description}, qr/\w/, 'miss needs description')
798 192 50       967 unless $event->{penaltyshot};
799 192         433 continue;
800             }
801 1952         4509 when ([ qw(PENL) ]) {
802 128         338 test_penalty($event, $opts);
803 128         397 continue;
804             }
805             }
806              
807             }
808              
809             sub test_event ($;$) {
810              
811 1952     1952 1 2791 my $event = shift;
812 1952   50     3472 my $opts = shift || {};
813              
814 1952         2398 $EVENT = $event;
815 1952         7080 my_like($event->{period}, qr/^\d$/, 'event period ok');
816 1952         6753 test_time($event->{time}, 'event time ok');
817 1952         8651 test_event_strength($event, $opts, "event $event->{type}/$event->{time}");
818 1952         4620 test_event_coords($event);
819 1952         4937 test_event_description($event);
820 1952         5527 my_ok($VOCABULARY{events}->{$event->{type}}, 'valid type');
821 1952         5510 test_assists_and_servedby($event, $opts);
822 1952         4237 test_event_by_type($event, $opts);
823 1952         3719 undef $EVENT;
824             }
825              
826             sub test_events ($;$) {
827              
828 9     9 1 37 my $events = shift;
829 9   100     37 my $opts = shift || {};
830              
831 9         21 my $event_n = scalar @{$events};
  9         26  
832              
833             my_ok($event_n >= $REASONABLE_EVENTS{
834             $BOXSCORE->{season} < 2010 ? 'old' : 'new'
835             }, " $BOXSCORE->{_id} enough events($event_n) read")
836             unless
837             $ZERO_EVENT_GAMES{$BOXSCORE->{_id}} ||
838             ($BROKEN_FILES{$BOXSCORE->{_id}}{BS} && $BROKEN_FILES{$BOXSCORE->{_id}}{BS} == $NO_EVENTS) &&
839             (!$BOXSCORE->{sources}{GS} && !$BOXSCORE->{sources}{PL})
840 9 100 33     252 || $opts->{bh} || $opts->{gs};
    100 0        
      33        
      33        
      66        
      100        
841 9         31 for my $event (@{$events}) {
  9         35  
842 1952         3111 test_event($event, $opts);
843             }
844 9         35 undef $EVENT;
845             }
846              
847             sub test_boxscore ($;$) {
848              
849 6     6 1 157 my $boxscore = shift;
850 6   50     21 my $opts = shift || {bs => 0};
851              
852 6         130 $BOXSCORE = $boxscore;
853 6         28 test_header($boxscore);
854 6 100       23 test_periods($boxscore->{periods}) if $opts->{bs};
855             test_officials($boxscore->{officials}, $opts)
856 6 100 100     58 if ! $opts->{es} && ! $opts->{pl} && $boxscore->{season} >= $DATA_BY_SEASON{officials};
      100        
857 6 100       32 test_teams($boxscore->{teams}, $opts) if ! $opts->{pl};
858             test_events($boxscore->{events}, $opts) unless
859 6 100 33     11656 $BROKEN_FILES{BS}->{$BOXSCORE->{_id}} && $BROKEN_FILES{BS}->{$BOXSCORE->{_id}} == $NO_EVENTS || $opts->{es} || $opts->{ro};
      66        
      100        
860 6         13 undef $BOXSCORE;
861 6         8 undef $PLAYER;
862 6         20 undef $EVENT;
863             }
864              
865             sub test_merged_header ($) {
866              
867 3     3 1 5 my $bs = shift;
868 3         21 test_header($bs);
869              
870 3 100 66     45 my_like($bs->{attendance}, qr/^\d+$/, 'attendance set')
871             if $BOXSCORE->has_html() || ! is_unapplicable('attendance');
872 3 50       16 my_like($bs->{tz}, qr/^\w{1,2}T$/, 'tz ok') if $bs->has_html();
873 3         19 my_like($bs->{month}, qr/^(0|1)?\d?/, 'month ok');
874             }
875              
876             sub test_merged_teams ($) {
877              
878 3     3 1 6 my $teams = shift;
879 3         25 my $opts = {merged => 1};
880 3         22 test_teams($teams, $opts);
881             }
882              
883             sub test_merged_events ($) {
884              
885 4     4 1 18 my $events = shift;
886 4         13 my $opts = {merged => 1};
887              
888 4         23 test_events($events, $opts);
889             }
890              
891             sub test_merged_boxscore ($) {
892              
893 3     3 1 9818 my $boxscore = shift;
894 3         1974 $BOXSCORE = $boxscore;
895 3         36 test_merged_header($boxscore);
896 3         16 test_merged_teams($boxscore->{teams});
897 3         17 test_periods($boxscore->{periods});
898 3         12 test_merged_events($boxscore->{events});
899 3         6 undef $BOXSCORE;
900 3         6 undef $EVENT;
901 3         11 undef $PLAYER;
902             }
903              
904             sub test_consistency_penalty_minutes ($$) {
905              
906 38     38 1 36 my $roster_player = shift;
907 38         36 my $event_player = shift;
908              
909 38   100     84 $event_player->{penaltyMinutes} ||= 0;
910 38   100     88 $event_player->{servedbyMinutes} ||= 0;
911             my_is_one_of(
912             $roster_player->{penaltyMinutes},
913             [
914             $event_player->{penaltyMinutes},
915             $event_player->{penaltyMinutes} + $event_player->{servedbyMinutes},
916             $event_player->{penaltyMinutes} - $event_player->{servedbyMinutes},
917             ],
918             "Player $roster_player->{_id}/$roster_player->{name} penaltyMinutes consistent"
919 38 50 33     158 ) if defined $roster_player->{penaltyMinutes} && $roster_player->{penaltyMinutes} != -1;
920 38 100       94 if ($roster_player->{penaltyMinutes} == $event_player->{penaltyMinutes} - $event_player->{servedbyMinutes}) {
921 37         46 $roster_player->{penaltyMinutes} += $event_player->{servedbyMinutes};
922             }
923             }
924              
925             sub test_consistency_goalie ($$$) {
926              
927 2     2 1 4 my $roster_player = shift;
928 2         2 my $event_player = shift;
929 2         3 my $boxscore_id = shift;
930              
931             my_is(
932             $roster_player->{shots} - $roster_player->{saves},
933             $event_player->{goalsAgainst} || 0,
934             "Player $roster_player->{_id}/$roster_player->{name} goalsAgainst consistent"
935 2 50 50     6 ) unless $BROKEN_FILES{$boxscore_id}->{BS} || is_unapplicable('saves');
      33        
936             }
937              
938             sub test_consistency_skater ($$$$) {
939              
940 36     36 1 38 my $roster_player = shift;
941 36         33 my $event_player = shift;
942 36         33 my $boxscore_id = shift;
943 36         33 my $stats = shift;
944              
945 36         35 for my $stat (@{$stats}) {
  36         48  
946 108 100       138 next if $stat eq 'penaltyMinutes';
947 72 100 66     147 if ($stat eq 'goals' || $stat eq 'assists') {
948             my_is(
949             $roster_player->{$stat},
950 36   100     118 $event_player->{$stat} || 0,
951             "Player $roster_player->{_id}/$roster_player->{name} $stat consistent"
952             );
953 36         69 return;
954             }
955 36 50       50 next unless defined $roster_player->{$stat};
956             my_is_one_of(
957             $roster_player->{$stat},
958             [
959             $event_player->{$stat} - 1,
960             $event_player->{$stat},
961             $event_player->{$stat} + 1,
962             ],
963             "Player $roster_player->{_id}/$roster_player->{name} $stat consistent"
964 36 50 33     68 ) unless $BROKEN_FILES{BS}->{$boxscore_id} || is_unapplicable($stat);
965             }
966             }
967              
968             sub test_consistency_playergoals ($$) {
969              
970 1     1 1 3 my $boxscore = shift;
971 1         2 my $event_summary = shift;
972              
973 1         3 for my $t (0, 1) {
974 2         3 my $team = $boxscore->{teams}[$t];
975 2         3 for my $player (@{$team->{roster}}) {
  2         5  
976 43   100     123 $player->{goals} ||= 0;
977 43 100       50 if ($player->{position} eq 'G') {
978             $event_summary->{$team->{name}}{playergoals} +=
979 2   50     15 ($event_summary->{$player->{_id}}{g_goals} || 0);
980             }
981             else {
982 41         58 $event_summary->{$team->{name}}{playergoals} += $player->{goals};
983             }
984             }
985             my_is(
986             $team->{score},
987 2         10 $event_summary->{$team->{name}}{playergoals} + $event_summary->{so}[$t],
988             "Team $team->{name} ($t) playergoals consistent",
989             );
990             }
991             }
992              
993             sub test_consistency ($$) {
994              
995 1     1 1 3 my $boxscore = shift;
996 1         1 my $event_summary = shift;
997              
998 1         3 $THIS_SEASON = $boxscore->{season};
999 1         2 $BOXSCORE = $boxscore;
1000 1         3 for my $t (0,1) {
1001 2         3 my $team = $boxscore->{teams}[$t];
1002             my_is(
1003             ($event_summary->{$team->{name}}{score} || 0),
1004             $team->{score},
1005             "Team $team->{name} score $team->{score} consistent"
1006 2 50 50     16 ) unless $BROKEN_FILES{$boxscore->{_id}}->{BS};
1007 2         6 for my $player (@{$team->{roster}}) {
  2         6  
1008 43 100 66     102 next if $player->{broken} || $player->{position} eq 'N/A';
1009 38         40 $PLAYER = $player;
1010 38         72 test_consistency_penalty_minutes($player, $event_summary->{$player->{_id}});
1011             $player->{position} eq 'G' ?
1012             test_consistency_goalie($player, $event_summary->{$player->{_id}}, $boxscore->{_id}) :
1013 38 100       80 test_consistency_skater($player, $event_summary->{$player->{_id}}, $boxscore->{_id}, $event_summary->{stats});
1014             }
1015 2         6 undef $PLAYER;
1016             }
1017             test_consistency_playergoals($boxscore, $event_summary)
1018 1 50       5 unless $BROKEN_FILES{$boxscore->{_id}}->{BS};
1019             }
1020              
1021             sub test_normalized_header ($) {
1022              
1023 3     3 1 9 my $boxscore = shift;
1024              
1025 3 50       28 if ($boxscore->{teams}[0]{score} > $boxscore->{teams}[1]{score}) {
    50          
1026 0         0 my_is($boxscore->{result}[0], 2, 'winner correct in result');
1027 0 0 0     0 my_is($boxscore->{result}[1], $boxscore->{season} > 1998 && $boxscore->{ot} ? 1 : 0, 'loser correct in result');
1028             }
1029             elsif ($boxscore->{teams}[0]{score} < $boxscore->{teams}[1]{score}) {
1030 3         25 my_is($boxscore->{result}[1], 2, 'winner correct in result');
1031 3 50 66     31 my_is($boxscore->{result}[0], $boxscore->{season} > 1998 && $boxscore->{ot} ? 1 : 0, 'loser correct in result');
1032             }
1033             else {
1034 0         0 my_is($boxscore->{result}[0], 1, 'tie correct in result');
1035 0         0 my_is($boxscore->{result}[1], 1, 'tie correct in result');
1036             }
1037 3         31 my_like($boxscore->{date}, qr/^\d{8}$/, 'game date set correctly');
1038 3 100       25 my_ok($boxscore->{location}, 'location set') unless is_unapplicable('location');
1039 3         17 my $path = get_game_path_from_id($boxscore->{_id});
1040 3         19 for my $source (qw(BS PL RO GS ES)) {
1041             my_is($boxscore->{sources}{$source}, 1 , "source $source registered")
1042 15 100 66     234 if $source eq 'BS' || (-f "$path/$source.html" && ! $BROKEN_FILES{$boxscore->{_id}}{$source});
      100        
1043             }
1044 3         11 for my $field (qw(_id attendance last_updated month date ot start_ts stop_ts stage season season_id)) {
1045 33         124 my_like($boxscore->{$field}, qr/^\-?\d+$/, "$field a number");
1046             }
1047             }
1048              
1049             sub test_normalized_roster ($$) {
1050              
1051 6     6 1 11 my $roster = shift;
1052 6         12 my $team_name = shift;
1053              
1054 6         12 for my $player (@{$roster}) {
  6         22  
1055 106         163 for (keys %{$player}) {
  106         615  
1056 3022         4636 my $field = $_;
1057 3022         4504 when ('position') { test_position($player->{$_}, 'position ok') }
  106         217  
1058 2916         3550 when ('name') { test_name($player->{$_}, 'name ok') };
  106         232  
1059 2810         3304 when ('status') {
1060 76         197 my_like($player->{$field}, qr/^(C|A| |X)$/, 'status ok');
1061             }
1062 2734         3136 when ('start') {
1063 76         193 my_like($player->{$field}, qr/^(0|1|2)$/, 'start ok');
1064             }
1065 2658         3007 when ('plusMinus') {
1066 72         189 my_like($player->{$field}, qr/^\-?\d+$/, '+- ok');
1067             }
1068 2586         2839 when ('decision') {
1069 6 50       25 if ($player->{position} eq 'G') {
1070 6         34 test_decision($player->{$field}, 'decision ok');
1071             }
1072             else {
1073 0         0 my_die("skater $player->{_id} should not have decision");
1074             }
1075             }
1076 2580         2889 when ('team') {
1077 106         214 my_is($player->{team}, $team_name, 'team in player ok');
1078             }
1079 2474         2712 default {
1080             my_like(
1081             $player->{$field},
1082             qr/[+-]?([0-9]*[.])?[0-9]+/, "stat $field a number"
1083 2474 50       8635 ) if defined $player->{$field};
1084             }
1085             }
1086             }
1087             }
1088              
1089             sub test_normalized_teams ($) {
1090              
1091 3     3 1 8 my $boxscore = shift;
1092 3         14 for my $t (0,1) {
1093 6         22 my $team = $boxscore->{teams}[$t];
1094 6         12 for my $stat (keys %{$team->{stats}}) {
  6         40  
1095 50         186 my_like($team->{stats}{$stat}, qr/[+-]?([0-9]*[.])?[0-9]+/, "team $stat a number");
1096             }
1097 6         25 for my $field (qw(pull shots score)) {
1098 18         80 my_like($team->{$field}, qr/[+-]?([0-9]*[.])?[0-9]+/, "team $field a number");
1099             }
1100 6         27 my_ok(! exists $team->{_decision}, 'pseudo-decision removed');
1101 6         26 test_normalized_roster($team->{roster}, $team->{name});
1102             }
1103             }
1104              
1105             sub test_normalized_events ($) {
1106              
1107 3     3 1 10 my $boxscore = shift;
1108              
1109             return if $BROKEN_FILES{$boxscore->{_id}}->{BS} &&
1110 3 50 33     28 $BROKEN_FILES{$boxscore->{_id}}->{BS} == $UNSYNCHED;
1111 3         9 for my $event (@{$boxscore->{events}}) {
  3         16  
1112 666         1682 test_game_id($event->{game_id}, 'event has game');
1113 666 100       1659 my_like($event->{zone}, qr/^(OFF|DEF|NEU|UNK)$/, 'event has zone')
1114             unless is_noplay_event($event);
1115 666 100       1673 my_is(length($event->{strength}), 2, 'event has strength')
1116             unless is_noplay_event($event);
1117 666         1392 for my $field (qw(period season stage so ts)) {
1118             my_like($event->{$field}, qr/^\d+$/, "field $field a number")
1119 3330 100       10319 if defined $event->{$field};
1120             }
1121             test_event_coords($event)
1122 666 50       1181 if $event->{coords};
1123 666 100       1279 my_like($event->{t}, qr/^(-1|0|1)$/, 'event t index ok')
1124             unless is_noplay_event($event);
1125             my_like($event->{en}, qr/^(0|1)$/, 'event en ok')
1126 666 100       1574 if exists $event->{en};
1127             my_is(
1128             $event->{team2},
1129             $boxscore->{teams}[1-$event->{t}]{name}, 'team2 ok'
1130 666 100 100     2769 ) if defined $event->{t} && $event->{t} != -1;
1131 666         1268 for my $field (qw(player1 player2 assist1 assist2)) {
1132             test_player_id($event->{$field}, "field $field ok")
1133 2664 100       5017 if exists $event->{$field};
1134             }
1135 666 100       1090 if ($event->{on_ice}) {
1136 624         759 for my $t (0,1) {
1137 1248         1343 for my $o (@{$event->{on_ice}[$t]}) {
  1248         2269  
1138 7208         9328 test_player_id($o, 'valid player id on ice');
1139             }
1140             }
1141             }
1142 666         1084 for ($event->{type}) {
1143 666         1456 when ('GOAL') {
1144 11         31 test_player_id($event->{player1}, "goal scorer player1 ok");
1145             test_player_id($event->{player2}, "goal goalie player2 ok")
1146 11 50       53 unless $event->{en};
1147 11         34 for my $field (qw(en gwg penaltyshot)) {
1148 33         112 my_like($event->{$field}, qr/^0|1$/, "goal $field ok")
1149             }
1150 11 100       34 if ($event->{assist1}) {
1151 10         29 test_player_id($event->{assist1}, 'assist1 ok');
1152 10         42 my_is($event->{assist1}, $event->{assists}[0], 'in array');
1153 10 50       30 if ($event->{assist2}) {
1154 10         31 test_player_id($event->{assist2}, 'assist2 ok');
1155 10         38 my_is($event->{assist2}, $event->{assists}[1], 'in array');
1156             }
1157             }
1158 11         38 when ('PENL') {
1159             my_ok($event->{ps_penalty}, 'ps penalty')
1160 0 0       0 if $event->{length} == 0;
1161 0         0 test_penalty($event->{penalty}, 'penalty defined');
1162             test_player_id($event->{servedby}, 'servedby ok')
1163 0 0       0 if $event->{servedby};
1164             }
1165 11         29 when ('FAC') {
1166 0         0 test_team($event->{winning_team}, 'FAC winning team ok');
1167             }
1168 11 50       38 if ($event->{type} ne 'GOAL') {
1169 0         0 my_ok(!defined $event->{assist1}, 'no goal no assist1');
1170 0         0 my_ok(!defined $event->{assist2}, 'no goal no assist2');
1171 0         0 my_ok(!defined $event->{assists}, 'no goal no assists');
1172             }
1173             my_ok(
1174             $VOCABULARY{shot_type}->{$event->{shot_type}},
1175 11         66 "$event->{shot_type} shot type normalized",
1176             );
1177 11         24 my @fields = keys %{$event};
  11         103  
1178 11         27 for my $field (@fields) {
1179 325         748 my_ok(defined $field, "existing field $field defined");
1180 325 100 66     1030 next if $field eq 'file' || ref $event->{$field};
1181 279 100       663 if ($event->{$field} =~ /\D/) {
1182 88         206 my_is($event->{$field}, uc($event->{$field}), 'all UC ok');
1183             }
1184             else {
1185 191         438 my_like($event->{$field}, qr/^\d+$/, 'numeric field ok');
1186             }
1187             }
1188             }
1189             }
1190             }
1191             }
1192              
1193             sub test_arranged_events ($) {
1194              
1195 3     3 1 10 my $boxscore = shift;
1196              
1197 3         6 my $gp = scalar @{$boxscore->{periods}};
  3         12  
1198 3 50 0     13 $gp += $boxscore->{so} || 0 if $gp == 4;
1199 3         14 my_is($boxscore->{events}[-1]{type}, 'GEND', 'gend at the end');
1200 3         15 my_is($boxscore->{events}[-2]{type}, 'PEND', 'pend penultimate');
1201 3         9 my_is(scalar(grep{$_->{type} eq 'PSTR'} @{$boxscore->{events}}), $gp, "$gp pstr");
  666         1399  
  3         14  
1202 3         13 my_is(scalar(grep{$_->{type} eq 'PEND'} @{$boxscore->{events}}), $gp, "$gp pend");
  666         886  
  3         14  
1203 3         12 my_is(scalar(grep{$_->{type} eq 'GEND'} @{$boxscore->{events}}), 1, '1 gend');
  666         841  
  3         12  
1204              
1205 3         10 for my $e (0..$#{$boxscore->{events}}-1) {
  3         24  
1206             my_cmp_ok(
1207             $boxscore->{events}[$e]{period},
1208             '<=',
1209             $boxscore->{events}[$e+1]{period},
1210 663         2109 'period ordered'
1211             );
1212             my_cmp_ok(
1213             $boxscore->{events}[$e]{ts},
1214             '<=',
1215             $boxscore->{events}[$e+1]{ts},
1216             'ts ordered'
1217             ) if $boxscore->{events}[$e]{period} ==
1218 663 100       2955 $boxscore->{events}[$e+1]{period};
1219             my_cmp_ok(
1220             $Sport::Analytics::NHL::Normalizer::EVENT_PRECEDENCE{
1221             $boxscore->{events}[$e]{type}
1222             },
1223             '<=',
1224             $Sport::Analytics::NHL::Normalizer::EVENT_PRECEDENCE{
1225             $boxscore->{events}[$e+1]{type}
1226             },
1227             'precedence ordered'
1228             ) if
1229             $boxscore->{events}[$e]{period} ==
1230             $boxscore->{events}[$e+1]{period}
1231             && $boxscore->{events}[$e]{ts} ==
1232 663 100 100     3563 $boxscore->{events}[$e+1]{ts};
1233 663         1094 my $event = $boxscore->{events}[$e];
1234 663         2886 my_like($event->{_id}, qr/^$boxscore->{_id}\d{4}$/, '_id created');
1235 663 100       2642 if ($event->{type} eq 'PSTR') {
    100          
    50          
1236 9         40 my_like($event->{ts}, qr/^(0|\d{2,3}00)$/, 'period starts at 00');
1237 9         51 my_like($event->{time}, qr/^\d+:00$/, 'period starts at :00');
1238             }
1239             elsif ($event->{type} eq 'PEND') {
1240 9         26 my_ok($event->{ts}, 'pend timestamp defined');
1241             }
1242             elsif ($event->{type} eq 'GEND') {
1243 0         0 my_die "Should not get to GEND";
1244             }
1245             }
1246             }
1247              
1248             sub test_normalized_boxscore ($) {
1249              
1250 3     3 1 10398 my $boxscore = shift;
1251              
1252 3         10 $THIS_SEASON = $boxscore->{season};
1253 3         20 test_normalized_header($boxscore);
1254 3         15 test_normalized_teams($boxscore);
1255 3         22 test_normalized_events($boxscore);
1256 3         17 test_arranged_events($boxscore);
1257             }
1258              
1259             sub test_bio ($) {
1260              
1261 3     3 1 4 my $report = shift;
1262              
1263 3         13 test_player_id($report->{_id}, 'report player id ok');
1264 3         19 test_name($report->{name}, 'report playername ok');
1265 3         13 test_position($report->{position}, 'report position ok');
1266 3 50       23 my_like($report->{number}, qr/^\d{1,2}$/, "number $report->{number} ok") if defined $report->{number};
1267 3 50       25 my_like($report->{height}, qr/^\d+$/, "height $report->{height} ok") if defined $report->{height};
1268 3 50       21 my_like($report->{weight}, qr/^\d+$/, "weight $report->{weight} ok") if defined $report->{weight};;
1269 3         17 my_like($report->{shoots}, qr/^L|R$/, "shoots $report->{shoots} ok");
1270 3         17 my_like($report->{birthdate}, qr/^\-?\d+$/, "birthdate $report->{birthdate} ok");
1271 3         15 my_like($report->{city}, qr/^\S.*\S/, "city $report->{city} ok");
1272 3         15 my_like($report->{state}, qr/^\w\w$/, "state $report->{state} ok");
1273 3         15 my_like($report->{country}, qr/^\S.*\S/, "country $report->{country} ok");
1274 3         15 my_like($report->{active}, qr/^(0|1)$/, "active $report->{active} ok");
1275 3         21 my_like($report->{rookie}, qr/^(0|1)$/, "active $report->{rookie} ok");
1276 3 100       14 test_team_id($report->{team}, "name $report->{team} ok") if $report->{active};
1277 3         15 my_like($report->{pick}, qr/^\d{1,3}$/, "pick $report->{pick} ok");
1278 3 100       11 if ($report->{pick} == $UNDRAFTED_PICK) {
1279 1         4 my_is($report->{undrafted}, 1, 'player is undrafted');
1280             }
1281             else {
1282 2         8 test_team_id($report->{draftteam}, "draftteam $report->{draftteam} ok");
1283 2         14 my_like($report->{draftyear}, qr/^\d{4}$/, "year $report->{draftyear} ok");
1284 2         10 my_like($report->{round}, qr/^\d{1,2}$/, "round $report->{round} ok")
1285             }
1286             }
1287              
1288             sub test_career ($) {
1289              
1290 3     3 1 7 my $report = shift;
1291 3         6 my $n_career = $report->{career};
1292              
1293 3         6 for my $stage (@{$n_career}) {
  3         8  
1294 6         7 for my $season (@{$stage}) {
  6         10  
1295 160 50 33     424 if ($season->{season} ne 'total' && $season->{league} ne 'bogus') {
1296 160 100       277 next unless $season->{league} eq 'NHL';
1297 101   33     333 my_ok($season->{start} > 1890 && $season->{start} < $CURRENT_SEASON + 1, "Valid start $season->{start}");
1298 101   33     403 my_ok($season->{end} > 1890 && $season->{end} < $CURRENT_SEASON + 2, "Valid end $season->{end}");
1299 101 50       199 next unless length($season->{gp});
1300 101 50       258 my_ok($season->{gp} < 100, "reasonable gp $season->{gp}") if length($season->{gp});
1301 101 50       190 if ($report->{position} eq 'G') {
1302             my_ok($season->{w} < 80, "reasonable w $season->{w}")
1303 0 0       0 if length($season->{w});
1304             my_ok($season->{l} < 80, "reasonable l $season->{l}")
1305 0 0       0 if length($season->{l});
1306             my_ok($season->{t} < 80, "reasonable t $season->{t}")
1307 0 0       0 if length($season->{t});
1308             my_ok($season->{ot} < 80, "reasonable ot $season->{ot}")
1309 0 0 0     0 if $season->{ot} && length($season->{ot});
1310             my_ok($season->{so} < 50, "reasonable so $season->{so}")
1311 0 0       0 if length($season->{so});
1312             my_ok($season->{ga} < 500, "reasonable ga $season->{ga}")
1313 0 0       0 if length($season->{ga});
1314             }
1315             else {
1316             my_ok($season->{g} < 200, "reasonable g $season->{g}")
1317 101 50       266 if length($season->{g});
1318             my_ok($season->{a} < 200, "reasonable a $season->{a}")
1319 101 50       306 if length($season->{a});
1320             my_ok($season->{pim} < 1000, "reasonable pim $season->{pim}")
1321 101 50       287 if length($season->{pim});
1322             }
1323 101 100 66     308 if ($season->{league} eq 'NHL' && $season->{start} >= 1988) {
1324 68 50       106 if ($report->{position} eq 'G') {
1325 0 0 0     0 if (length($season->{gp}) && $season->{gp}) {
1326 0         0 my_ok($season->{gaa} < 200, "reasonable gaa $season->{gaa}");
1327 0         0 my_ok($season->{'sv%'} <= 1, "reasonable sv\% $season->{'sv%'}");
1328 0         0 my_ok($season->{sa} < 5000, "reasonable sa $season->{sa}");
1329 0         0 my_ok($season->{min} < 10000, "reasonable min $season->{min}");
1330             }
1331             }
1332             else {
1333 68         146 my_ok($season->{gwg} < 50, "reasonable gwg $season->{gwg}");
1334 68         177 my_ok($season->{shg} < 20, "reasonable shg $season->{shg}");
1335 68         173 my_ok($season->{ppg} < 50, "reasonable ppg $season->{ppg}");
1336             my_ok($season->{s} < 1000, "reasonable s $season->{s}")
1337 68 50       209 if length($season->{s});
1338             my_ok(
1339             $season->{'s%'} >= 0 && $season->{'s%'} <= 100,
1340             "reasonable s\% $season->{'s%'}"
1341 68 50 33     391 ) if $season->{s};
1342             my_like($season->{'+/-'}, qr/^\-?\d+$/, "reasonable +\/- $season->{'+/-'}")
1343 68 50       232 if length($season->{'+/-'});
1344             }
1345             }
1346             }
1347             else {
1348 0 0       0 next if $season->{league} eq 'bogus';
1349 0         0 my_is($season->{team}, 'NHL TOTALS', "valid $season->{team} pseudo team");
1350 0   0     0 my_ok($season->{career_start} >= $FIRST_SEASON && $season->{career_start} <= $CURRENT_SEASON,
1351             "Valid career_start $season->{career_start}");
1352 0   0     0 my_ok($season->{career_end} >= $FIRST_SEASON && $season->{career_end} <= $CURRENT_SEASON,
1353             "Valid career_end $season->{career_end}");
1354 0         0 my_is($season->{league}, 'NHL', 'only NHL totals are available');
1355             }
1356             }
1357             }
1358             }
1359              
1360             sub test_player_report ($) {
1361              
1362 3     3 1 25 my $report = shift;
1363              
1364 3         12 test_bio($report);
1365 3         10 test_career($report);
1366             }
1367              
1368             END {
1369 49 100   49   462304 if ($BOXSCORE) {
1370 1         3 $Data::Dumper::Varname = 'BOXSCORE';
1371             }
1372 49 50       350 if ($EVENT) {
1373 0         0 $Data::Dumper::Varname = 'EVENT';
1374 0         0 print Dumper $EVENT;
1375             }
1376 49 50       1837 if ($PLAYER) {
1377 0         0 $Data::Dumper::Varname = 'PLAYER';
1378 0         0 print Dumper $PLAYER;
1379             }
1380             }
1381              
1382             1;
1383              
1384             =head1 AUTHOR
1385              
1386             More Hockey Stats, C<< >>
1387              
1388             =head1 BUGS
1389              
1390             Please report any bugs or feature requests to C, or through
1391             the web interface at L. I will be notified, and then you'll
1392             automatically be notified of progress on your bug as I make changes.
1393              
1394              
1395             =head1 SUPPORT
1396              
1397             You can find documentation for this module with the perldoc command.
1398              
1399             perldoc Sport::Analytics::NHL::Test
1400              
1401             You can also look for information at:
1402              
1403             =over 4
1404              
1405             =item * RT: CPAN's request tracker (report bugs here)
1406              
1407             L
1408              
1409             =item * AnnoCPAN: Annotated CPAN documentation
1410              
1411             L
1412              
1413             =item * CPAN Ratings
1414              
1415             L
1416              
1417             =item * Search CPAN
1418              
1419             L
1420              
1421             =back