1

Okay so this is my current code which works, but I need to access each error hash in a different way in order to be compatible with other parts of the program. Here is my Error list library: Type.pm

package ASC::Builder::Error::Type;
  use strict;
  use warnings;
  use parent 'Exporter';

  # Export the list of errors
  our @EXPORT_OK = qw/
  UNABLE_TO_PING_SWITCH_ERROR
  /;
  # List of error messages
  use constant code => {

      CABLING_CHECK_TOR_INCORRECT_CABLING_ERROR => {
          category => 'Cabling Error',
          template => "ToR cabling is not correct at T1.The uplinks must be cabled to exactly one t1 device group",
          tt => { template => 'disabled'},
          fatal => 1,
          wiki_page =>'http://www.error-fix.com/',
      },
      UPDATE_IMAGE_ERROR => {
          category => 'Imaging Error',
          template => "Cannot determine switch model",
          tt => { template => 'disabled'},
          fatal => 1,
          wiki_page =>'http://www.error-fix.com/',
      },
      UNABLE_TO_PING_SWITCH_ERROR => {
          category => 'Connection Error',
          template => "Could not ping switch %s in %s seconds.",
          context => [ qw(switch_ip  timeout) ],
          tt => {template => 'disabled'},
          fatal => 1,
          wiki_page => 'http://www.error-fix.com/',
      },
      UNKNOWN_CLIENT_CERT_ID_ERROR => {
          category => 'Services Error',
          template => "Unknown client certificate id: %s",
          context => qw(cert_id),
          tt => { template => 'disabled'},
          fatal => 1,
          wiki_page =>'http://www.error-fix.com/',
      },
  # Add errors to this library
  };
  1;

Here is my Error.pm file. The new method is called for accessing and outputting a new error message and the rest are either getters or are called in the new method.

package ASC::Builder::Error;

  use strict;
  use warnings;
  use parent 'Exporter';
  our @EXPORT_OK = qw/new/;

  # Method for creating error message
  sub new {
      my ( $class, $error, %args ) = @_;
      # Initialize error with data
      my $self = $error;
      # If the error contains context parameters... Insert parameters into string template
      if( ref $self eq 'HASH' && %args) {
          foreach my $key (@{ $self->{context} } ) {
              # And take the ones we need
              $self->{args}->{$key} = $args{$key};
          }
          my @template_args = map { $self->{args}->{$_} } @{ $self->{context} };

          # map/insert arguments into context hash and insert into string template
          $self->{message} = sprintf ($self->{template}, @template_args);

      }
      return bless $self, $class;
  }


  # Accessor for category
  sub category {
      return shift->{category};
  }
  # Accessor for message
  sub template {
      return shift->{template};
  }
  # Accessor for context
  sub context {
      return shift->{context};
  }
  # Accessor for template option
  sub tt {
      return shift->{tt}{template};
  }
  # Accessor for fatal
  sub is_fatal {
      return shift->{fatal};
  }
  # Accessor for wiki_page
  sub wiki_page {
      return shift->{wiki_page};
  }
  # Accessor for args. args are a hash ref of context parameters that are
  # passed in as a list at construction
  sub args {
      return shift->{args};
  }
  # Builds the message string from the template. maps the input params from new
  # into context key

  #sub message {
  #    my ($self) = @_;
  #    return sprintf $self->template,
  #             map { $self->args->{$_} } @{ $self->context };
  #}
  sub message {
      return shift->{message};
  }
  # Stringifies the error to a log message (for SB dashboard), including the
  # category, message, and wiki_page.
  sub stringify {
      my $self = @_;
      return sprintf ("%s: %s\nMore info: %s",$self->{category}, $self->{message}, $self->{wiki_page});
  }
  1;

I will also include my test (where I am running this program & testing the error output). This also shows how an error is called. In the systems code it would be called like so:

ASC::Builder:Error->new(UNABLE_TO_PING_SWITCH_ERROR, switch_ip => 192.192.0.0, timeout => 30);

Error.t

#!/usr/bin/env perl

  use lib ('./t/lib');
  use strict;
  no strict 'refs';
  use warnings;

  use ASC::Builder::Error;
  use ASC::Builder::Error::Type;
  use Test::More;
  use Test::Exception;
  use LWP::Simple 'head'; # Used to test if wiki link is giving a response

  subtest 'Functionality of Error' => sub {
      my $example_error = {
              category => 'Connection Error',
              template => 'Could not ping switch %s in %s seconds.',
              context => [ qw(switch_ip  timeout) ],
              tt => {template => 'disabled'},
              fatal => 1,
              wiki_page => 'http://www.error-fix.com/',
      };

      # Correct case
      {
          my $error = ASC::Builder::Error->new( code => $example_error, timeout => 30, switch_ip => '192.192.0.0' );

          isa_ok ($error, 'ASC::Builder::Error');

          can_ok ($error, 'category');
          is ($error->category(), 'Connection Error', 'Return the correct category');

          can_ok ($error, 'template');
          is ($error->template(), 'Could not ping switch %s in %s seconds.', 'Return the correct category');

          can_ok ($error, 'tt');
          is ($error->tt(), 'disabled', 'Return the correct tt template');

          can_ok ($error, 'context');
          is_deeply($error->context(), ['switch_ip', 'timeout'], 'Return the correct context params');

          can_ok ($error, 'is_fatal');
          ok($error->is_fatal(), 'Return the correct value');

          can_ok ($error, 'message');
          is ($error->message(), 'Could not ping switch 192.192.0.0 in 30 seconds.', 'Return the correct message');
          can_ok ($error, 'stringify');
          is ($error->stringify(), "Connection Error : Could not ping switch 192.192.0.0 in 30 seconds.\nMore info: http://www.error-fix.com/" , 'stringify creates the correct message');

  };

      # Too many arguments (this is okay)
      lives_ok( sub { ASC::Builder::Error->new($example_error, timeout => 1, switch_ip => 2, extra => 3 ) }, 'Creating with too many arguments lives. (allows for additional context string   to be added in the code)' );
      };

      subtest 'Correctness of Type.pm' => sub {

  # These test cases contain all the errors from Type.pm
      my @test_cases = (
         {
              name => 'UNABLE_TO_PING_SWITCH_ERROR',
              args => {
                  switch_ip => '192.192.0.0',
                  timeout => 30,
              },
              message => 'Could not ping switch 192.192.0.0 in 30 seconds.',
          },
      );


      foreach my $t (@test_cases) {
          subtest $t->{name} => sub {
              no strict 'refs'; # Because we need to use variable to get to a constant
              ASC::Builder::Error::Type->import($t->{name});

              # Create the Error object from the test data
              # Will also fail if the name was not exported by Type.pm
              my $error;
              lives_ok( sub { $error = ASC::Builder::Error->new( &{ $t->{name} },%{ $t->{args} }) }, 'Error can be created');

              # See if it has the right values
              is ($error->message, $t->{message}, 'Error message is correct');

              # Using LWP::Simple to check if the wiki page link is not broken
              #ok head($error->wiki_page); #CANT'T GET THIS TEST TO WORK

          }
      }
  };
  done_testing;

I am trying to change it so that I can call each error something like:

ASC::Builder:Error->new(code => UNABLE_TO_PING_SWITCH_ERROR, switch_ip => 192.192.0.0, timeout => 30);
Paul Russell
  • 179
  • 10

2 Answers2

1

Your constructor expects that you pass it the following arguments: scalar, hash. The scalar is then used in the code as a hashref

my ($class, $error, %args) = @_;
my $self = $error;
# If the error contains  [...]
if (ref $self eq 'HASH' && %args) 

When you call it with

ASC::Builder:Error->new(UNABLE_TO_PING_SWITCH_ERROR, ...

that is exactly what is happening and all is well. If you want to call it as

ASC::Builder:Error->new(code => UNABLE_TO_PING_SWITCH_ERROR, ...

then you'd be passing a whole hash to it, with an even number of elements. There is no hashref (scalar) first. The constructor as it stands should give you an error about a list with odd number of elements assigned to hash, as it will first take the scalar string 'code' into $error and then attempt to assign the remaining list, UNABLE.., ... to a hash. Alas, that rest now has an odd number of elements what doesn't work for a hash. Remember that (a => 'A', b => 'B') is the same as ('a', 'A', 'b', 'B'), and when a is removed the rest can't be a hash any more.

If you want to call it that way and have the processing in your constructor the same, you'd need to change the constructor to first fetch the value of the key 'code' from the submitted hash (into $error) and remove that element from it, so that the rest can then be assigned to %args, for later processing. Some example code would be

my ($class, %args) = @_;
my $self = delete $args{code};
# Now %args contains what is needed by existing code

The delete removes the element from hash, and returns it.

delete EXPR
Given an expression that specifies an element or slice of a hash, delete deletes the specified elements from that hash so that exists() on that element no longer returns true. Setting a hash element to the undefined value does not remove its key, but deleting it does; see exists.
[...]
In list context, returns the value or values deleted, or the last such element in scalar context.

You can also support both calling conventions, by pre-processing @_ once $class has been shift-ed from it. If it does not contain a hashref first, you do the above (or something like it), otherwise you need not. Your current processing stays as it is. For example

my $class = shift;
my ($self, %args);
if (ref $_[0] eq 'HASH') {
    $self = shift @_;
    %args = @_;
}
else {     
    %args = @_;        
    $self = delete $args{code};     
}

You can add more checking at this point. The above can be done differently, I tried to keep it clear.

zdim
  • 64,580
  • 5
  • 52
  • 81
  • This solution seems to be working. Thanks a lot. I never thought of taking code as an argument and then deleting it. Great idea. – Paul Russell May 20 '16 at 14:15
  • I can get this to work for my first subtest, but I can't figure out how I should access the error through `code` in my second subtest. – Paul Russell May 20 '16 at 14:47
  • I have since got it to work. But I still receive the "Odd number of elements" warning. Is this because there is an odd number off elements including 'code' and then code is deleted? – Paul Russell May 20 '16 at 16:36
  • @PaulRussell Why do you have in test, `code => { my $example_error = { category => ...` ? This is a very complicated data structure. Why not just `code => { category => ... ` -- so that `code` is a key with the value being a hashref which is precisely your `Type.pm` instance. The class I posted presumes that, sorry I didn't notice this test business. I'll look through it more carefully. – zdim May 21 '16 at 04:27
  • @PaulRussell Is `'Functionality of Error'` giving you trouble? Are you getting the error on the first line under `# Correct case`, where you call `new`? – zdim May 21 '16 at 08:52
  • 1
    @PaulRussell As for _odd number of elements..._, see the following. (1) this is a hash -- `%h = (1, one, 2, two)`. (2) This is **not** a hash -- `%h = ('hey', 1, one, 2, two)` -- you get error "_odd number ..._". This is what happens when you send `new->(code => { ...}, %hash)` _and then use it_ as `($scalar, %hash) = @_ `. With `new->(code => ...` (above) you pass a hash. When you take off an element it's not any more. What you pass in the `Functionality of Error` test is even more complicated and seems to unravel as odd number of elements, being assigned to a would-be hash. – zdim May 22 '16 at 08:13
  • My tests all pass but give me warnings that there is an odd number of elements, will this be a problem. If so, I am not sure how I fix this? – Paul Russell May 24 '16 at 16:23
  • @PaulRussell Sure, it can be a problem. It's probably something with how the test is set up (per my comment above), I'll look into it. – zdim May 25 '16 at 02:41
  • Oh sorry, I didn't read the above I missed it. Maybe if I had the `$error` variable back in and then delete code from that , which then allows it to take the error hash as it's first argument, would that work. Since this I've had to add an is_error check to the start of this method, but I don't think it should affect what I am trying to achieve here as it is only a check. And in my data structure I actually am not using this data structure for my example error `use constant code =>` , I have removed the constant list wrapping the error. I have modified the code accordingly. – Paul Russell May 25 '16 at 11:53
  • @PaulRussell Ah, I see now that you saw that comment. I'll look through both. In short: the last code snippet allows you to call `new` either way, as `new(%hash)` or `new($hashref, %some_hash)`. As for my comment further above, it seems from your new question that you did clean up that complicated construction in the test. You still pass a list to the constructor at one place, and the constructor isn't ready to take that. – zdim May 25 '16 at 19:24
0

In new, this is what you have (from your test):

$error = code; # Scalar or Hash Reference (depending on scope of code constant)
%args = (UNABLE_TO_PING_SWITCH_ERROR, switch_ip => 192.192.0.0, timeout => 30);

Which is not what I think you want to have:

$args{UNABLE_TO_PING_SWITCH_ERROR} = 'switch_ip';
$args{'192.192.0.0'} = 'timeout';
$args{30} = undef;

This should be true even with code and UNABLE_TO_PING_SWITCH_ERROR being either scalars and/or a hash references.

You need to alter new to determine if the first one/two arguments are hash references and/or there is an even number of arguments after it.

kjpires
  • 730
  • 4
  • 14
  • So do I specify `$error = 'code'` at the start of my new method, or will I just be inserting that at the start of my test. If I'm putting it in at the start of my test , how would I carry this over outside of testing? Do you know what I'm asking ? Also I'm not sure if I fully understand what's happening in your alteration of the new method ? – Paul Russell May 19 '16 at 15:49
  • The only line I dont really understand what's happening in is `unshift(@args, 'code') if (@args % 2);` – Paul Russell May 19 '16 at 15:52
  • I think your biggest problem is the scope of the `code` constant: I believe it is local to the package and thus it is just the scalar `'code'` when you call `new`, but I believe you are thinking the constant is global and a hash reference everywhere. – kjpires May 19 '16 at 15:54
  • You should put a break point before the first line of code in `new` and inspect your arguments: that will give you more details of why your code failed. – kjpires May 19 '16 at 15:58