Thursday, 25 July 2013

A Look at Dumping

I used Data::Dumper a lot but it leaves something to be desired. By adding a function to MyUtils.pm, a simpler interface can be achieved.

Last time, I created a test script for trim() in the directory ~/perl5/lib/t/MyUtils/. The second part of this article will create a second test script to test the function created here. To run all the test script, use the program, prove, which is installed with Perl.

  $ prove -r ~/perl5/lib/t

This will recursively search through all the subdirectories looking for programs ending with *.t and run them. It will produce a summary of all the tests.

A Function with A Friendlier Interface

Data::Dumper-Dump()> allows you to put your own tags on the values it prints, but its interface seems a little crude. Wouldn't it be nicer if it allowed the format of tag => $value? And as an added bonus, simpler control over its options.

So, I created vardump() with the following usage:

  Usage: $text = vardump( ?%options, $tag => \$value, ... );

This first argument, ?%options is an optional reference to a hash which contains the options for the function. I can do this since all tags are expected to be scalars. That means they can be distinguished from any hash reference.

The remaining arguments are in pairs. First there is a scalar containing the tag, then there is its value, which can be a scalar or a reference.

Writing the Code

The function will be added to MyUtils.pm.

Adding to @EXPORTS_OK

First, open MyUtils.pm in your favourite editor and add vardump to @EXPORT_OK.

  our @EXPORT_OK = qw(
      trim
      vardump
  );

Now add a brief description of what it is:

  # --------------------------------------
  #       Name: vardump
  #      Usage: $text = vardump( ?%options, $tag => \$value, ... );
  #    Purpose: A simply interface to Data::Dumper->Dump
  # Parameters: ?%options -- optional hash ref of options
  #                  $tag -- A variable name or other tag
  #                $value -- A scalar or a reference
  #                   ... -- repeat tag-value pairs as needed
  #    Returns:     $text -- dumped variables
  #

Place the function is a block to isolate the scoping of the default options. Technically, this is called a static closure.

  # create a block to limit scoping
  {
      use Data::Dumper;

      my %default_options = (
          -depth    => 0,  # for $Data::Dumper::Maxdepth
          -indent   => 1,  # for $Data::Dumper::Indent
          -purity   => 0,  # for $Data::Dumper::Purity
          -sortkeys => 1,  # for $Data::Dumper::Sortkeys
      );

The default options are set to my preferences. You can, of course, change them to yours. See perldoc Data::Dumper for details.

Now start the function:

      sub vardump {

First, check for optional options and process them. The code uses a slice to copy the given options to the hash.

          # Check for optional options
          my %options = %default_options;
          if( ref( $_[0] ) && ref( $_[0] ) eq 'HASH' ){
              my %given_options = %{ shift @_ };

              # use a slice to copy
              @options{ keys %given_options } = values %given_options;
          }

Now, we come to the heart of the function: to create the arrays used by Data::Dumper->Dump.

          # create two arrays for Data::Dumper->Dump
          my @tags   = ();
          my @values = ();

          # process the remaining arguments
          while( @_ ){
              my $tag   = shift @_;
              my $value = shift @_;

              # if value is a ref, make Dump() conform to correct type
              $tag = "*$tag" if ref( $value );

              # add them to the lists
              push @tags,   $tag;
              push @values, $value;
          }

Localize the Data::Dumper variables so they'll revert back to what they were before this subroutine.

          # localize Data::Dumper options
          local $Data::Dumper::Indent   = $options{ -indent   };
          local $Data::Dumper::Maxdepth = $options{ -depth    };
          local $Data::Dumper::Purity   = $options{ -purity   };
          local $Data::Dumper::Sortkeys = $options{ -sortkeys };

Finally, we're ready to call Dump() and returns its output.

          return Data::Dumper->Dump( \@values, \@tags );

And finish the subroutine and scoping block.

      } # end sub
  } # end scoping block

Writing the Tests for the Function

Create the test script and make it executable.

  $ cd ~/perl5/lib/t/MyUtils
  $ >01-vardump.t
  $ chmod a+x 01-vardump.t

Now, open the file with your favourite editor. Start with the following:

  #!/usr/bin/env perl

  use strict;
  use warnings;

Set up the testing environment.

  use Test::More;
  BEGIN{ use_ok( 'MyUtils' ); } # test #1: check to see if module can be compiled
  my $test_count = 1; # 1 for the use_ok() in BEGIN

  use MyUtils qw( vardump ); # import the vardump() function

Now to add some tests.

Scalar Test

A block is used to isolate the variables.

  # block for isolation of variables
  {
      my $var = 'test';

      my $actual   = vardump( scalar => $var );
      my $expected = Data::Dumper->Dump( [ $var ], [ 'scalar' ] );

      is( $actual, $expected, 'scalar test' );
      $test_count ++;
  }

Array Test

Note that the Data::Dumper variable has to be set to the defaults of vardump.

  # Array test
  {
      my @var = qw{ fee fie foe fue };

      my $actual   = vardump( array => \@var );

      local $Data::Dumper::Indent = 1;
      my $expected = Data::Dumper->Dump( [ \@var ], [ '*array' ] );

      is( $actual, $expected, 'array test' );
      $test_count ++;
  }

Hash Test

Note that the Data::Dumper variable has to be set to the defaults of vardump.

  # Hash test
  {
      my %var = qw{ fee fie foe fue };

      my $actual   = vardump( hash => \%var );

      local $Data::Dumper::Indent   = 1;
      local $Data::Dumper::Sortkeys = 1;
      my $expected = Data::Dumper->Dump( [ \%var ], [ '*hash' ] );

      is( $actual, $expected, 'hash test' );
      $test_count ++;
  }

Depth Option Test

  # Depth option test
  {
      my @var = ( 0, [ 1, [ 2, [ 3 ]]] );

      my $actual = vardump( { -depth=>2, }, depth => \@var );

      local $Data::Dumper::Indent   = 1;
      local $Data::Dumper::Sortkeys = 1;
      local $Data::Dumper::Maxdepth = 2;
      my $expected = Data::Dumper->Dump( [ \@var ], [ '*depth' ] );

      is( $actual, $expected, 'depth option test' );
      $test_count ++;
  }

Finally...

At the end, tell Test::More how many tests were done.

  # tell Test::More we're done
  done_testing( $test_count );

No comments:

Post a Comment