Test::Stream::Plugin::Explain::Terse - Dump anything in a single line in 80 characters or fewer


version 0.001001


  use Test::Stream::Bundle::V1;
  use Test::Stream::Plugin::Explain::Terse qw( explain_terse );

  note "Studying: fn(y) = " . explain_terse(my $got = fn($y));

  # test $got



This module aims to provide a simple tool for adding trace-level details about data-structures to the TAP stream to visually keep track of what is being tested.

Its objective is to not be comprehensive, and only be sufficient for a quick visual sanity check, allowing you to visually spot obviously wrong things at a glance without producing too much clutter.

It is expected that if Explain::Terse produces a data structure that needs compacting for display, that the user will also be performing sub-tests on that data structure, and those sub-tests will trace their own context closer to the actual test.

  # Checking: { a_key => ["a value"], b_key => ["b value'], ... }
    # Subtest: c_key is expected
    # Checking: ["c value"]
    ok 1 - c_key's array has value "c value"
  ok 1 - c_key is expected

The idea being the higher up in the data structure you're doing the comparison, the less relevant the individual details are to that comparison, and the actual details only being relevant in child comparisons.

This is obviously also better if you're doing structurally layered comparison, and not simple path-based comparisons, e.g:

  # Not intended to be used this way.
  note explain_terse(\%hash);
  is( $hash{'key'}{'otherkey'}{'finalkey'}, 'expected_value' );

And you want something like:

  note explain_terse(\%hash);
  ok( exists $hash{'key'}, 'has q[key]')
    and subtest "key structure" => sub {

      my $structure = $hash{'key'};
      note explain_terse($structure);
      is( ref $structure, 'HASH', 'is a HASH' )
        and ok( exists $structure->{'otherkey'}, 'has q[otherkey]' )
        and subtest "otherkey structure" => sub {

          my $substructure = $structure->{'otherkey'};
          note explain_terse($substructure);
          is( ref $substructure, 'HASH', 'is a HASH' )
            and ok( exists $structure->{'finalkey'}, 'has final key' )
            and subtest "finalkey structure" => sub {

              my $final_structure = $substructure->{'finalkey'};
              note explain_terse($final_structure);
              ok( !ref $final_structure, "finalkey is not a ref")
                and ok( defined $final_structure, "finalkey is defined")
                and is( $final_structure, 'expected_value', "finalkey is expected_value" );


Though of course you'd not want to write it like that directly in your tests, you'd probably want something more like

  with(\%hash)->is_hash->has_key('key', sub {
      with($_[0])->is_hash->has_key('otherkey', sub {
        with($_[0])->is_hash->has_key('finalkey', sub {


  cmp_deeply( \%hash, superhashof({
      key => superhashof({
        otherkey => superhashof({
          finalkey => "expeted_value"

And have Explain::Terse operating transparently under the hood of these implementations so you can see what is happening.



  my $data = explain_terse($structure);

Returns $structure pretty printed and compacted to be less than 80 characters long.



This module intends to inter-operate with Test::Stream which this modules author considers still in a heavy state of flux, and so this module cannot be considered even remotely stable until some point after that becoming more stable.

Dumper internals.

This module presently uses pp from Data::Dump as its main formatter bolted into some simple sub-string operations and newline transformations.

It is planned that this module will switch to using Data::Dumper at some future time, pending on its addition of features like range-list reductions, and other niceties Data::Dump offers.

Alas, Data::Dump doesn't support sub de-parsing, and Data::Dump doesn't have internals that could be considered a canonical reference implementation Data::Dumper is.

So as soon as Data::Dumper has all the features this module wants, it will switch.

But you shouldn't be relying on the output of this module having a fixed string representation anyway, its purely for human consumption.

Controlled Non-Terse Dumping

Two features here could be useful, but I'm still working out how to do it nicely.

  • It would be nice to stash diag traces in a context and then reveal the entire leg of the test prior to the failure, but only on failure, such that when you were just reading a passing TAP series it wasn't burdensome, but when failures occurred you got all the details you needed still.

  • Conditionally diaging in full uncondensed form might eventually be a feature at user request.

And the above two in conjunction could be really handy.



The author of Test::Stream presently indicates their preferred way of consuming plugins like this one would be as follows:

  use Test::Stream -V1, 'Explain::Terse';

The author of this module finds such a style confusing an unclear to new users and finds it seriously impedes automatic prerequisite detection.

  use Test::Stream::Bundle::V1, 'Explain::Terse';

This style is less confusing, but not yet perfectly clear.

  use Test::Stream::Bundle::V1;
  use Test::Stream::Plugin::Explain::Terse qw( explain_terse );

is much more obvious what is happening.


This module presently uses Test::Stream::Exporter as its exporter library. This is for inter-operability with the Test::Stream bundling system which allows for bundles to compose multiple plugins into a single calling class.

This technique requires a bit of indirection, and requires allowing the bundle to clearly communicate the name of the bundles caller to its composed plugins while allowing plugins to augment that callers name-space directly.

But to facilitate this, a specific non-import interface must exist on the plugin which the Test::Stream infrastructure can use to permit explicit passing of caller() data without needing to pull cute tricks like locally redefining caller() like Sub::Uplevel, or imposing limitations on the ->import(@ARGS) syntax, and avoids needing to do strange import tricks like Import::Into does with eval.


  020: sub import {
  021:   my $class = shift;
  022:   my @caller = caller;
  024:   push @_ => $class->default unless @_;
  026:   $class->load(\@caller, @_);
  028:   1;
  029: }
  030: sub load {
  140: if ($mod->can('load_ts_plugin')) {
  141:   $mod->load_ts_plugin($caller, @$import);
  142: }
  143: elsif (my $meta = Test::Stream::Exporter::Meta->get($mod)) {
  144:   Test::Stream::Exporter::export_from($mod, $caller->[0], $import);
  145: }


  09: default_export import => sub {
  10:    my $class = shift;
  11:    my @caller = caller;
  13:    my $bundle = $class;
  14:    $bundle =~ s/^Test::Stream::Bundle::/-/;
  16:    require Test::Stream;
  17:    Test::Stream->load(\@caller, $bundle, @_);
  18: };


Kent Fredric <>


This software is copyright (c) 2015 by Kent Fredric <>.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.